<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Colby</title>
    <description>The latest articles on DEV Community by David Colby (@davidcolbyatx).</description>
    <link>https://dev.to/davidcolbyatx</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F510445%2Ff91e2161-c944-4001-bcc4-2be8fa3aa689.jpg</url>
      <title>DEV Community: David Colby</title>
      <link>https://dev.to/davidcolbyatx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davidcolbyatx"/>
    <language>en</language>
    <item>
      <title>User notifications with Rails, Noticed, and Hotwire</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Tue, 22 Mar 2022 02:36:31 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/user-notifications-with-rails-noticed-and-hotwire-39ga</link>
      <guid>https://dev.to/davidcolbyatx/user-notifications-with-rails-noticed-and-hotwire-39ga</guid>
      <description>&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="https://github.com/excid3/noticed" rel="noopener noreferrer"&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="https://turbo.hotwired.dev/reference/streams" rel="noopener noreferrer"&gt;Turbo Streams&lt;/a&gt; and, for extra fun, we will load user notifications inside of a &lt;a href="https://turbo.hotwired.dev/reference/frames" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5eet623ucrkvnlt8z6i.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5eet623ucrkvnlt8z6i.gif" alt="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 submittd"&gt;&lt;/a&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;/p&gt;

&lt;h2&gt;
  
  
  Application setup
&lt;/h2&gt;

&lt;p&gt;To follow along with this tutorial, start by cloning &lt;a href="https://github.com/DavidColby/user-notices" rel="noopener noreferrer"&gt;this repository from Github&lt;/a&gt; and then set it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;user-notices
bin/setup
&lt;/code&gt;&lt;/pre&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&gt;3.0.2&lt;/code&gt;, but everything in this tutorial will work fine with Ruby &lt;code&gt;2.7&lt;/code&gt; and &lt;code&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&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&gt;bin/dev&lt;/code&gt;.&lt;/p&gt;

&lt;h2&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&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="http://localhost:3000/users/sign_up" rel="noopener noreferrer"&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&gt;Messages&lt;/code&gt; for other users in the application. Each time a message is a created, a new &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add noticed
rails generate noticed:model
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These commands are straight from the Noticed &lt;a href="https://github.com/excid3/noticed#-installation" rel="noopener noreferrer"&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&gt;bundle add&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, update &lt;code&gt;app/models/user.rb&lt;/code&gt; to associate notifications with users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;devise&lt;/span&gt; &lt;span class="ss"&gt;:database_authenticatable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:registerable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="ss"&gt;:recoverable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:rememberable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:validatable&lt;/span&gt;

  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :recipient&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we added the &lt;code&gt;notifications&lt;/code&gt; &lt;code&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&gt;Message&lt;/code&gt; resource. From your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g scaffold message content:text user:references
rails db:migrate
&lt;/code&gt;&lt;/pre&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="https://github.com/rails/tailwindcss-rails" rel="noopener noreferrer"&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&gt;User&lt;/code&gt; model again at &lt;code&gt;app/models/user.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;devise&lt;/span&gt; &lt;span class="ss"&gt;:database_authenticatable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:registerable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="ss"&gt;:recoverable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:rememberable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:validatable&lt;/span&gt;

  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:messages&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :recipient&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we added &lt;code&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&gt;app/views/dashboard/show.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user_signed_in?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    Signed in as &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;. &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="s2"&gt;"Sign out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destroy_user_session_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :delete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"All messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Sign in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_user_session_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then make a small adjustment to the messages form so we don’t have to memorize user ids:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"contents"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"error_explanation"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;pluralize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; prohibited this message from being saved:&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_message&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_area&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;rows: &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:user_id&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options_for_select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these changes in place, head to &lt;a href="http://localhost:3000/messages" rel="noopener noreferrer"&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&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails generate noticed:notification MessageNotification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generator creates a new &lt;code&gt;MessageNotification&lt;/code&gt; class at &lt;code&gt;app/notifications/message_notification.rb&lt;/code&gt;. Head there next and make a few small updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MessageNotification&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Noticed&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;deliver_by&lt;/span&gt; &lt;span class="ss"&gt;:database&lt;/span&gt;

  &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:message&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;
    &lt;span class="n"&gt;message_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&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&gt;deliver_by :email, mailer: SomeMailer&lt;/code&gt;, as described in the Noticed &lt;a href="https://github.com/excid3/noticed/blob/master/docs/delivery_methods/email.md" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;param :message&lt;/code&gt; serializes the &lt;code&gt;message&lt;/code&gt; object and stores it with the notification record in the database. We use that serialized &lt;code&gt;message&lt;/code&gt; in the &lt;code&gt;message&lt;/code&gt; and &lt;code&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&gt;params&lt;/code&gt; and profit.&lt;/p&gt;

&lt;p&gt;We can now use our &lt;code&gt;MessageNotification&lt;/code&gt; to send notifications to users when a new message is created. Next we need to put the &lt;code&gt;MessageNotification&lt;/code&gt; class into use. Head to &lt;code&gt;app/models/message.rb&lt;/code&gt; and update it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_noticed_notifications&lt;/span&gt;

  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="n"&gt;after_create_commit&lt;/span&gt; &lt;span class="ss"&gt;:notify_user&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;notify_user&lt;/span&gt;
    &lt;span class="no"&gt;MessageNotification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each time a message is created (&lt;code&gt;after_create_commit&lt;/code&gt;), &lt;code&gt;notify_user&lt;/code&gt; runs and creates a new &lt;code&gt;MessageNotification&lt;/code&gt;, serializing the &lt;code&gt;message&lt;/code&gt; object and delivering the message to the message’s user. We also add &lt;a href="https://github.com/excid3/noticed#associating-notifications" rel="noopener noreferrer"&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g controller Notifications index
&lt;span class="nb"&gt;touch &lt;/span&gt;app/views/notifications/_notification.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Head to &lt;code&gt;config/routes.rb&lt;/code&gt; and add a notifications path helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:messages&lt;/span&gt;
  &lt;span class="n"&gt;devise_for&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'dashboard/show'&lt;/span&gt;
  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="s2"&gt;"dashboard#show"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;NotificationsController&lt;/code&gt; at &lt;code&gt;app/controllers/notifications_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@notifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;recipient: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we scope &lt;code&gt;notifications&lt;/code&gt; to the &lt;code&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&gt;app/views/notifications/index.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"notifications"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Notifications&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@notifications&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&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="https://turbo.hotwired.dev/reference/frames#eager-loaded-frame" rel="noopener noreferrer"&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&gt;render @notifications&lt;/code&gt; relies on Rails’ &lt;a href="https://guides.rubyonrails.org/v7.0/layouts_and_rendering.html#rendering-collections" rel="noopener noreferrer"&gt;collection rendering&lt;/a&gt; to render each notification. Before this will work, we need to fill in &lt;code&gt;app/views/notifications/_notification.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-700"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between mt-1 text-gray-500 text-sm space-x-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
        Received on &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_date&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
        Status: &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"Read"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Unread"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we use &lt;code&gt;to_notification&lt;/code&gt; from Noticed to access the &lt;code&gt;message&lt;/code&gt; method we added in &lt;code&gt;MessageNotification&lt;/code&gt; earlier, and use the built-in &lt;code&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user_signed_in?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      Signed in as &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;. &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="s2"&gt;"Sign out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destroy_user_session_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :delete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"All messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"notifications"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;notifications_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Sign in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_user_session_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;turbo_frame_tag&lt;/code&gt; has an id of &lt;code&gt;notifications&lt;/code&gt;, which matches the Turbo Frame rendered by &lt;code&gt;Notifications#index&lt;/code&gt;. When Turbo eager-loads content for a Turbo Frame, it expects the url passed to &lt;code&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&gt;Dashboard#show&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Dashboard#show&lt;/code&gt; is loaded&lt;/li&gt;
&lt;li&gt;Turbo sees the &lt;code&gt;turbo_frame_tag&lt;/code&gt; with an &lt;code&gt;src&lt;/code&gt; attribute and initiates a new request to &lt;code&gt;/notifications&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Notifications#index&lt;/code&gt; returns HTML that includes a &lt;code&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&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpn9lgvu6bx3u289qmf84.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpn9lgvu6bx3u289qmf84.png" alt="A screenshot of a web page open to a list of notifications under a Notifications heading"&gt;&lt;/a&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&gt;
  
  
  Real-time notifications with Turbo Streams
&lt;/h2&gt;

&lt;p&gt;Turbo model broadcasts, powered by &lt;a href="https://github.com/hotwired/turbo-rails" rel="noopener noreferrer"&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&gt;app/models/notification.rb&lt;/code&gt; to trigger a Turbo Stream broadcast when a new notification is created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Noticed&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Model&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;polymorphic: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;after_create_commit&lt;/span&gt; &lt;span class="ss"&gt;:broadcast_to_recipient&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;broadcast_to_recipient&lt;/span&gt;
    &lt;span class="n"&gt;broadcast_append_later_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;:notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s1"&gt;'notifications-list'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'notifications/notification'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;notification: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the &lt;code&gt;broadcast_to_recipient&lt;/code&gt; method &lt;code&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&gt;recipient, :notifications&lt;/code&gt;, as described in the &lt;code&gt;turbo-rails&lt;/code&gt; &lt;a href="https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb#L22" rel="noopener noreferrer"&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&gt;notifications-list&lt;/code&gt; id (matching the &lt;code&gt;target&lt;/code&gt; passed to &lt;code&gt;broadcast_append_later_to&lt;/code&gt;) for the Turbo Stream to update.&lt;/p&gt;

&lt;p&gt;Starting in &lt;code&gt;app/views/notifications/index.html.erb&lt;/code&gt;, add the &lt;code&gt;notifications-list&lt;/code&gt; id to the &lt;code&gt;ul&lt;/code&gt; containing the list of notifications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"notifications"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Notifications&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notifications-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@notifications&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then in &lt;code&gt;app/views/dashboard/show.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user_signed_in?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      Signed in as &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;. &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="s2"&gt;"Sign out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destroy_user_session_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :delete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"All messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:notifications&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"notifications"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;notifications_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Sign in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_user_session_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the &lt;code&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&gt;current_user, :notifications&lt;/code&gt; in the view == &lt;code&gt;recipient, :notifications&lt;/code&gt; in the model).&lt;/p&gt;

&lt;p&gt;When a user visits the dashboard, the &lt;code&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&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="http://localhost:3000" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Turbo::StreamsChannel is streaming from Z2lkOi8vdXNlci1ub3RpY2VzL1VzZXIvMQ:notifications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another browser, head to the &lt;a href="http://localhost:3000/messages/new" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpav5m99bty6z79vtffa.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpav5m99bty6z79vtffa.gif" alt="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 submittd."&gt;&lt;/a&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&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&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&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="https://gorails.com/episodes/rails-notifications-with-noticed" rel="noopener noreferrer"&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="https://github.com/excid3/noticed" rel="noopener noreferrer"&gt;repo’s readme&lt;/a&gt; and digging in to specific &lt;a href="https://github.com/excid3/noticed/tree/master/docs/delivery_methods" rel="noopener noreferrer"&gt;delivery methods&lt;/a&gt; as needed&lt;/li&gt;
&lt;li&gt;Solidify Turbo concepts with my &lt;a href="https://www.colby.so/posts/turbo-rails-101-todo-list" rel="noopener noreferrer"&gt;Turbo 101 article&lt;/a&gt;, and my &lt;a href="https://www.colby.so/posts/turbo-frames-on-rails" rel="noopener noreferrer"&gt;Turbo Frames&lt;/a&gt; and &lt;a href="https://www.colby.so/posts/turbo-streams-on-rails" rel="noopener noreferrer"&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="https://davidcolby.gumroad.com/l/hotwired-ats" rel="noopener noreferrer"&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;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Toggling view layouts with Kredis, Turbo Frames, and Rails</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Tue, 08 Mar 2022 02:31:25 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/toggling-view-layouts-with-kredis-turbo-frames-and-rails-1077</link>
      <guid>https://dev.to/davidcolbyatx/toggling-view-layouts-with-kredis-turbo-frames-and-rails-1077</guid>
      <description>&lt;p&gt;Kredis is a new &lt;a href="https://github.com/rails/kredis"&gt;gem&lt;/a&gt; that makes it easier to work with Redis keys in Ruby on Rails. Kredis &lt;a href="https://github.com/rails/rails/commit/5d9ee652013e527129792016c103ee1133106276"&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="https://github.com/rails/kredis#kredis"&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;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mPvRlv-o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0exkcjh3mw26q10536m4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mPvRlv-o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0exkcjh3mw26q10536m4.gif" alt="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." width="880" height="512"&gt;&lt;/a&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="https://github.com/DavidColby/kredis_view_toggle/tree/final"&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start building.&lt;/p&gt;

&lt;h2&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&gt;Players&lt;/code&gt; resource, which we will use as the base for our Kredis-powered view toggle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new kredis-players &lt;span class="nt"&gt;--css&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tailwind
&lt;span class="nb"&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;/div&gt;



&lt;p&gt;Note the inclusion of the &lt;code&gt;css=tailwind&lt;/code&gt; option in the &lt;code&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&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&gt;db/seeds.rb&lt;/code&gt; and update it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"Player &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="ss"&gt;team: &lt;/span&gt;&lt;span class="s2"&gt;"Dallas Mavericks"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then seed the database from your terminal:&lt;/p&gt;

&lt;h2&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&gt;app/views/players/_player.html.erb&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"border border-gray-200 rounded"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"block hover:bg-gray-50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center px-4 py-4 sm:px-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-0 flex-1 flex items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-sm font-medium"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden md:block"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-sm text-gray-900"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;team&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"View"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-700 hover:text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just regular ERB and some Tailwind classes for styling. Now update &lt;code&gt;app/views/players/index.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Players&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New player'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_player_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"space-y-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&gt;_card&lt;/code&gt; partial. Create the new partial from your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;app/views/players/_card.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then fill in the new &lt;code&gt;_card&lt;/code&gt; partial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-span-1 flex shadow-sm rounded-md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 px-4 py-2 text-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-900 font-medium hover:text-gray-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;team&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"px-4 py-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"View"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-700 hover:text-blue-500"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;app/views/players/index.html.erb&lt;/code&gt; and update it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Players&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New player'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_player_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full mt-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:view&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"card"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"List view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Card view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"space-y-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;collection: &lt;/span&gt;&lt;span class="vi"&gt;@players&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"players/card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :player&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we add a messy but functional &lt;code&gt;if&lt;/code&gt; statement that checks the value of &lt;code&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&gt;link_to players_path(view: "list"/"card")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This “works” as a starting point. Head to &lt;a href="http://localhost:3000/players"&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&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;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2r8BED9r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xectzfoap0wpwkql804h.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2r8BED9r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xectzfoap0wpwkql804h.gif" alt="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's preference." width="880" height="512"&gt;&lt;/a&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&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&gt;/players&lt;/code&gt; without any URL parameters.&lt;/p&gt;

&lt;p&gt;This is where &lt;code&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;- &lt;span class="c"&gt;# gem "kredis"&lt;/span&gt;
+ gem &lt;span class="s2"&gt;"kredis"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then from your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt;
./bin/rails kredis:install
&lt;/code&gt;&lt;/pre&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="https://gist.github.com/tomysmile/1b8a321e7c58499ef9f9441b2faa0aa8"&gt;this gist&lt;/a&gt; helpful. Linux users may find &lt;a href="https://www.ubuntupit.com/how-to-install-and-configure-redis-on-linux-system/"&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&gt;app/controllers/players_controller.rb&lt;/code&gt; and update the &lt;code&gt;index&lt;/code&gt; action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
  &lt;span class="vi"&gt;@user_preferences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Kredis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'preferences'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@user_preferences&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:view&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:view&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are using Kredis to fetch a &lt;code&gt;preferences&lt;/code&gt; key from Redis, initializing it as a &lt;a href="https://github.com/rails/kredis/blob/20aa4f272763151d462e9c9e125fcebf2eed4a5d/lib/kredis/types/hash.rb#L3"&gt;hash&lt;/a&gt; and setting the &lt;code&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&gt;params[:view]&lt;/code&gt; is present in the request, we update the &lt;code&gt;preferences&lt;/code&gt; hash to store the value of &lt;code&gt;view&lt;/code&gt;. &lt;code&gt;update&lt;/code&gt; here updates the &lt;code&gt;preferences&lt;/code&gt; key in Redis along with updating the &lt;code&gt;user_preferences&lt;/code&gt; instance variable.&lt;/p&gt;

&lt;p&gt;Because &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:012:0&amp;gt; preferences &lt;span class="o"&gt;=&lt;/span&gt; Kredis.hash&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'preferences'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;#&amp;lt;Kredis::Types::Hash:0x00007faed43f0f90&lt;/span&gt;
...
irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:013:0&amp;gt; preferences.keys
  Kredis Proxy &lt;span class="o"&gt;(&lt;/span&gt;0.3ms&lt;span class="o"&gt;)&lt;/span&gt;  HKEYS preferences
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:014:0&amp;gt; preferences.update&lt;span class="o"&gt;(&lt;/span&gt;view: &lt;span class="s1"&gt;'card'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  Kredis Proxy &lt;span class="o"&gt;(&lt;/span&gt;0.2ms&lt;span class="o"&gt;)&lt;/span&gt;  HSET preferences &lt;span class="o"&gt;[{&lt;/span&gt;:view&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="o"&gt;}]&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1
irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:015:0&amp;gt; preferences.keys
  Kredis Proxy &lt;span class="o"&gt;(&lt;/span&gt;0.3ms&lt;span class="o"&gt;)&lt;/span&gt;  HKEYS preferences
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"view"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:016:0&amp;gt; preferences[&lt;span class="s1"&gt;'view'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
  Kredis Proxy &lt;span class="o"&gt;(&lt;/span&gt;0.3ms&lt;span class="o"&gt;)&lt;/span&gt;  HGET preferences &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"view"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"card"&lt;/span&gt;
irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:017:0&amp;gt; preferences.delete&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'view'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  Kredis Proxy &lt;span class="o"&gt;(&lt;/span&gt;0.3ms&lt;span class="o"&gt;)&lt;/span&gt;  HDEL preferences &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"view"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1
irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:018:0&amp;gt; preferences.keys
  Kredis Proxy &lt;span class="o"&gt;(&lt;/span&gt;0.2ms&lt;span class="o"&gt;)&lt;/span&gt;  HKEYS preferences
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;app/views/players/index.html.erb&lt;/code&gt; and replace the existing &lt;code&gt;if&lt;/code&gt; block with the below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@user_preferences&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:view&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"card"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"List view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@players&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"players/card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :player&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Card view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"space-y-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we updated the &lt;code&gt;if&lt;/code&gt; condition to check &lt;code&gt;@user_preferences&lt;/code&gt; instead of &lt;code&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="http://localhost:3000/players"&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&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="https://turbo.hotwired.dev/handbook/frames"&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="https://turbo.hotwired.dev/handbook/drive"&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="http://localhost:3000/players"&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;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KX-8T1O_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/irnu22y82ajdpk2k8trj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KX-8T1O_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/irnu22y82ajdpk2k8trj.gif" alt="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." width="880" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This flash of content happens because Turbo Drive caches the index page before navigating from &lt;code&gt;/players&lt;/code&gt; to &lt;code&gt;/players?view=card&lt;/code&gt;. The next time you visit &lt;code&gt;/players&lt;/code&gt;, Drive uses the original, list-view version of &lt;code&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&gt;app/views/players/index.html.erb&lt;/code&gt; to wrap the list of players in a Turbo Frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Players&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New player'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_player_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"players"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"min-w-full mt-8"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@user_preferences&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:view&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"card"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"List view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@players&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"players/card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :player&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Card view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"space-y-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we removed the “players” div and replaced that div with a &lt;code&gt;&amp;lt;turbo-frame id="players"&lt;/code&gt;, using the turbo-rails provided &lt;code&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&gt;players&lt;/code&gt; Turbo Frame from the response and use that content to update the &lt;code&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&gt;card&lt;/code&gt; and &lt;code&gt;player&lt;/code&gt; partials like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"View"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-700 hover:text-blue-500"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;turbo_frame: &lt;/span&gt;&lt;span class="s2"&gt;"_top"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The addition of &lt;code&gt;data-turbo-frame="_top"&lt;/code&gt; to links wrapped inside of a &lt;code&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&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="http://localhost:3000/players"&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;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oMVF2xSV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/coqvd78h4ghcqz067t80.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oMVF2xSV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/coqvd78h4ghcqz067t80.gif" alt="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." width="880" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&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&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&gt;User&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./bin/rails g model User session:string
./bin/rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then update &lt;code&gt;app/controllers/application_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;helper_method&lt;/span&gt; &lt;span class="ss"&gt;:current_user&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;current_user&lt;/span&gt;    
    &lt;span class="vi"&gt;@current_user&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_or_create_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;session: &lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&gt;session.id&lt;/code&gt; to find or create a new user in the database and then defining &lt;code&gt;current_user&lt;/code&gt; as a &lt;code&gt;helper_method&lt;/code&gt; available throughout our application.&lt;/p&gt;

&lt;p&gt;In a real application, &lt;code&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&gt;User&lt;/code&gt; using Kredis. Our first implementation of Kredis hardcoded a &lt;code&gt;preferences&lt;/code&gt; key to store all preferences. Our new implementation will give each &lt;code&gt;User&lt;/code&gt; their own unique key through Kredis’ integration with ActiveRecord.&lt;/p&gt;

&lt;p&gt;Head to &lt;code&gt;app/models/user.rb&lt;/code&gt; and update it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;kredis_hash&lt;/span&gt; &lt;span class="ss"&gt;:preferences&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;preferences&lt;/code&gt; in Redis. We can access this attribute with &lt;code&gt;user.preferences&lt;/code&gt;, which will return an instance of a &lt;code&gt;Kredis::Hash&lt;/code&gt;, just like &lt;code&gt;Kredis.hash('preferences')&lt;/code&gt; did in our controller.&lt;/p&gt;

&lt;p&gt;To use this new &lt;code&gt;preferences&lt;/code&gt; attribute, update the &lt;code&gt;PlayersController#index&lt;/code&gt; action in &lt;code&gt;app/controllers/players_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
  &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:view&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:view&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we use the same &lt;code&gt;update&lt;/code&gt; method we used before, this time using &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Players&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New player'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_player_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"players"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"min-w-full mt-8"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:view&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"card"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"List view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@players&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"players/card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :player&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Card view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;view: &lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"space-y-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;if&lt;/code&gt; statement now checks &lt;code&gt;current_user.preferences&lt;/code&gt; instead of &lt;code&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&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="https://github.com/rails/kredis"&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="https://github.com/julianrubisch/cubism"&gt;presence tracking&lt;/a&gt; for application resources or &lt;a href="https://allfutures.leastbad.com/"&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;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Turbo Rails 101: Building a todo app with Turbo</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Fri, 11 Feb 2022 17:11:42 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/turbo-rails-101-building-a-todo-app-with-turbo-5fn2</link>
      <guid>https://dev.to/davidcolbyatx/turbo-rails-101-building-a-todo-app-with-turbo-5fn2</guid>
      <description>&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fasak5gmieeuqcwh8qinw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fasak5gmieeuqcwh8qinw.gif" alt="A screen recording of a user of a web application adding and removing todo items from a list of todos"&gt;&lt;/a&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="https://github.com/DavidColby/turbo-todo" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start building!&lt;/p&gt;

&lt;h2&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new turbo-todo &lt;span class="nt"&gt;--css&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tailwind
&lt;span class="nb"&gt;cd &lt;/span&gt;turbo-todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then scaffold up a &lt;code&gt;Todo&lt;/code&gt; resource. From your terminal again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g scaffold Todo name:string status:integer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update migration to set default value for status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateTodos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:todos&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;

      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, create and migrate the database:&lt;/p&gt;

&lt;h2&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&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&gt;app/views/todos/index.html.erb&lt;/code&gt;, with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl text-gray-900"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Your todos
  &lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full max-w-2xl bg-gray-100 py-8 px-4 border border-gray-200 rounded shadow-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"todos"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@todos&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; below the form.&lt;/p&gt;

&lt;p&gt;Note that the &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; has an id of &lt;code&gt;todos&lt;/code&gt;. Turbo Streams target elements in the DOM by id, and the &lt;code&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&gt;app/views/todos/_todo.html.erb&lt;/code&gt; to render each todo properly inside of the &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-4 border-b border-gray-300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;form&lt;/code&gt; partial we render in the index view needs a few adjustments too. In &lt;code&gt;app/views/todos/_form.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_form"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"error_explanation"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;pluralize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; prohibited this todo from being saved:&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_message&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-stretch flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"sr-only"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block w-full rounded-none rounded-l-md sm:text-sm border-gray-300"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"Add a new todo..."&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"-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"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the addition of an &lt;code&gt;id&lt;/code&gt; to the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, using the &lt;code&gt;dom_id&lt;/code&gt; of the &lt;code&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&gt;TodosController&lt;/code&gt; and update the &lt;code&gt;create&lt;/code&gt; action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="vi"&gt;@todo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;todo_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Todo was successfully created."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change here is the addition of &lt;code&gt;format.turbo_stream&lt;/code&gt; to the happy path in the action. &lt;code&gt;format.turbo_stream&lt;/code&gt; tells Rails that when a Turbo Stream request is sent to the &lt;code&gt;create&lt;/code&gt; action, respond with a matching &lt;code&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&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;app/views/todos/create.turbo_stream.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then fill that new file in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt; &lt;span class="s2"&gt;"todos"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"todo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_form"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our first look at a Turbo Stream! In &lt;code&gt;create.turbo_stream.erb&lt;/code&gt; we render two &lt;code&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;The first &lt;a href="https://turbo.hotwired.dev/reference/streams#prepend" rel="noopener noreferrer"&gt;prepends&lt;/a&gt; the newly created todo to the list of &lt;code&gt;todos&lt;/code&gt;, targeting the &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; with the id of &lt;code&gt;todos&lt;/code&gt;. The second &lt;a href="https://turbo.hotwired.dev/reference/streams#replace" rel="noopener noreferrer"&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&gt;bin/dev&lt;/code&gt;. Head to &lt;a href="http://localhost:3000/todos" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnn5i7uy1tbkrslbpj5eu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnn5i7uy1tbkrslbpj5eu.gif" alt="A screen recording of a user of a web application adding a todo item to the list of todos"&gt;&lt;/a&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="https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses" rel="noopener noreferrer"&gt;Accept header&lt;/a&gt; that identifies the request as a Turbo Stream request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;text/vnd.turbo-stream.html, text/html, application/xhtml+xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turbo sets this header automatically on all &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; form submissions, with no intervention required from the developer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hotwired/turbo-rails/blob/2a2a5ec12e4233cf33e5d3f0a6855834f4e5e4ec/lib/turbo/engine.rb#L51" rel="noopener noreferrer"&gt;turbo-rails&lt;/a&gt; registers a &lt;code&gt;turbo_stream&lt;/code&gt; Mime type to enable responding to inbound Turbo Stream form submissions with &lt;code&gt;turbo_stream&lt;/code&gt; content. We see this in action in the &lt;code&gt;TodosController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When calling &lt;code&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&gt;create.turbo_stream.erb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;create.turbo_stream.erb&lt;/code&gt;, we render &lt;code&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; elements using the &lt;code&gt;turbo_stream&lt;/code&gt; helper. Rails renders the &lt;code&gt;create.turbo_stream&lt;/code&gt; view to HTML and sends that HTML back to the frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"prepend"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"todos"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-4 border-b border-gray-300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      A new todo
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"replace"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"new_todo_form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"new_todo_form"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/todos"&lt;/span&gt; &lt;span class="na"&gt;accept-charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"authenticity_token"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"TmpPYfxQOln1t3jmigbzZ49ciBWjivGEjp_nJUWJMeUJZSoeA8dvbkLeD6kLkmHZx-zc8kOzcqu69tWGYASlpQ"&lt;/span&gt; &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-stretch flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"todo_name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Name&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"block w-full rounded-none rounded-l-md sm:text-sm border-gray-300"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Add a new todo..."&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"todo[name]"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"todo_name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"commit"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Create Todo"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"-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"&lt;/span&gt; &lt;span class="na"&gt;data-disable-with=&lt;/span&gt;&lt;span class="s"&gt;"Create Todo"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;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&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&gt;&amp;lt;tr&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; elements in &lt;code&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="https://github.com/hotwired/turbo/pull/113" rel="noopener noreferrer"&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&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&gt;turbo-rails&lt;/code&gt;, developers can easily &lt;code&gt;broadcast&lt;/code&gt; updates from models and background jobs to send &lt;code&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; snippets over WebSockets with  &lt;code&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&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&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rpso65k4ohqchsf1hdn.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rpso65k4ohqchsf1hdn.gif" alt="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."&gt;&lt;/a&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_container"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-4 border-b border-gray-300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we added a unique id to each &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;. Nested within the &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;, we added a &lt;code&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; using the &lt;code&gt;turbo-rails&lt;/code&gt; provided &lt;code&gt;turbo_frame_tag&lt;/code&gt; helper method.&lt;/p&gt;

&lt;p&gt;Within the Turbo Frame is a &lt;code&gt;link_to&lt;/code&gt; pointing to the &lt;code&gt;edit&lt;/code&gt; action in the &lt;code&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&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&gt;app/views/edit.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;turbo_frame_tag&lt;/code&gt; has an id that matches the &lt;code&gt;turbo_frame_tag&lt;/code&gt; in the &lt;code&gt;todo&lt;/code&gt; partial.&lt;/p&gt;

&lt;p&gt;With that change in place, refresh the &lt;code&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&gt;TodosController#update&lt;/code&gt;.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;app/views/todos/update.turbo_stream.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the new &lt;code&gt;update.turbo_stream.erb&lt;/code&gt; view like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_container"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"todo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are using a Turbo Stream &lt;code&gt;replace&lt;/code&gt; action again. This time the Turbo Stream action replaces the content of the &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; wrapping the todo with the content of the &lt;code&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&gt;create.turbo_stream.erb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Update the &lt;code&gt;TodosController&lt;/code&gt; to respond to Turbo Stream requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;todo_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Todo was successfully updated."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:show&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;location: &lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&gt;else&lt;/code&gt; path in the &lt;code&gt;create&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt; actions is missing a &lt;code&gt;turbo_stream&lt;/code&gt; response. We will fix that in the next section.&lt;/p&gt;

&lt;h2&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&gt;Todo&lt;/code&gt; model so that we can send invalid form submissions to the server. In &lt;code&gt;app/models/todo.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;validates_presence_of&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Form submissions with a blank &lt;code&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&gt;TodosController&lt;/code&gt; and update the &lt;code&gt;create&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt; actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="vi"&gt;@todo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;todo_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Todo was successfully created."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;turbo_stream: &lt;/span&gt;&lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;todo_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Todo was successfully updated."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:show&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;location: &lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;turbo_stream: &lt;/span&gt;&lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the todo fails to save, we render a &lt;code&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&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_container"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-4 border-b border-gray-300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center space-x-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-red-600 px-4 py-2 rounded hover:bg-red-700"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :delete&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-5 w-5 text-white"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 20 20"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Delete todo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"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"&lt;/span&gt; &lt;span class="na"&gt;clip-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;method: :delete&lt;/code&gt; on the button, which ensures the button hits the &lt;code&gt;destroy&lt;/code&gt; action on the controller. The svg icon here is from &lt;a href="https://heroicons.com/" rel="noopener noreferrer"&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&gt;TodosController&lt;/code&gt;, update the &lt;code&gt;destroy&lt;/code&gt; action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;destroy&lt;/span&gt;
  &lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;

  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;turbo_stream: &lt;/span&gt;&lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_container"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;todos_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Todo was successfully destroyed."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:no_content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This inline &lt;code&gt;turbo_stream&lt;/code&gt; uses the Turbo Stream &lt;a href="https://turbo.hotwired.dev/reference/streams#remove" rel="noopener noreferrer"&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&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&gt;status&lt;/code&gt; enum in the &lt;code&gt;Todo&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;incomplete: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;complete: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back to &lt;code&gt;app/views/todos/_todo.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_container"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-4 border-b border-gray-300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center space-x-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'line-through'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-end space-x-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'incomplete'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-green-600 px-4 py-2 rounded hover:bg-green-700"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :patch&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Mark as incomplete&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-5 w-5 text-white"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 20 20"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"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"&lt;/span&gt; &lt;span class="na"&gt;clip-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'complete'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-gray-400 px-4 py-2 rounded hover:bg-green-700"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :patch&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Mark as incomplete&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-5 w-5 text-white"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 20 20"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"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"&lt;/span&gt; &lt;span class="na"&gt;clip-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-red-600 px-4 py-2 rounded hover:bg-red-700"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :delete&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-5 w-5 text-white"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 20 20"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Delete todo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"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"&lt;/span&gt; &lt;span class="na"&gt;clip-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'line-through'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the todo is complete, we render &lt;code&gt;button_to&lt;/code&gt; to mark the todo as incomplete. Incomplete todos get a &lt;code&gt;button_to&lt;/code&gt; to mark the todo as complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'incomplete'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-green-600 px-4 py-2 rounded hover:bg-green-700"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :patch&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'complete'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-gray-400 px-4 py-2 rounded hover:bg-green-700"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :patch&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In either case the &lt;code&gt;patch&lt;/code&gt; request goes to &lt;code&gt;TodosController#update&lt;/code&gt; as a Turbo Stream request, and the existing &lt;code&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&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&gt;index&lt;/code&gt; action in the &lt;code&gt;TodosController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="vi"&gt;@todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;presence&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'incomplete'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then update &lt;code&gt;app/views/todos/index.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl text-gray-900"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Your todos
  &lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"todos-container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block max-w-2xl w-full bg-gray-100 py-8 px-4 border border-gray-200 rounded shadow-sm"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"border-b border-gray-200 w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex space-x-2 justify-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Incomplete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;todos_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"incomplete"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"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"&lt;/span&gt; 
          &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;todos_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"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"&lt;/span&gt;
          &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"complete"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="no"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"todos"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@todos&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The index view now wraps the todo content in a &lt;code&gt;todos-container&lt;/code&gt; &lt;code&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&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&gt;todos-container&lt;/code&gt; Turbo Frame, each time those links are clicked, Turbo will replace the content of the &lt;code&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&gt;index&lt;/code&gt; action to render Turbo Frame content. Even though the entire page re-renders when the &lt;code&gt;index&lt;/code&gt; action is called, Turbo will extract the &lt;code&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="https://www.colby.so/posts/turbo-frames-on-rails#conditional-template-rendering-using-variants" rel="noopener noreferrer"&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&gt;TodosController&lt;/code&gt;. We will call this new action &lt;code&gt;change_status&lt;/code&gt;. Start in the &lt;code&gt;TodosController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[ show edit update destroy change_status ]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_status&lt;/span&gt;
  &lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="n"&gt;todo_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;turbo_stream: &lt;/span&gt;&lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_container"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;todos_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Updated todo status."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we updated the &lt;code&gt;set_todo&lt;/code&gt; &lt;code&gt;before_action&lt;/code&gt; to set the &lt;code&gt;@todo&lt;/code&gt; instance variable when &lt;code&gt;change_status&lt;/code&gt; is called and we defined &lt;code&gt;change_status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&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&gt;&amp;lt;li&amp;gt;&lt;/code&gt; and use a Turbo Stream &lt;code&gt;remove&lt;/code&gt; action.&lt;/p&gt;

&lt;p&gt;We added this new action because the &lt;code&gt;update&lt;/code&gt; action we used in the first implementation of this feature use a &lt;code&gt;replace&lt;/code&gt; Turbo Stream action, instead of removing the todo from the DOM. We could have hacked the &lt;code&gt;update&lt;/code&gt; action to handle status changes differently or created a whole new &lt;code&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&gt;config/routes.rb&lt;/code&gt; to add the new route to the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:todos&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;patch&lt;/span&gt; &lt;span class="ss"&gt;:change_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;on: :member&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, update the todo partial one last time to use the &lt;code&gt;change_status_todo_path&lt;/code&gt; on the status toggle buttons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;change_status_todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'incomplete'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-green-600 px-4 py-2 rounded hover:bg-green-700"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :patch&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Mark as incomplete&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-5 w-5 text-white"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 20 20"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"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"&lt;/span&gt; &lt;span class="na"&gt;clip-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;change_status_todo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;todo: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'complete'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-gray-400 px-4 py-2 rounded hover:bg-green-700"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :patch&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Mark as complete&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-5 w-5 text-white"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 20 20"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"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"&lt;/span&gt; &lt;span class="na"&gt;clip-rule=&lt;/span&gt;&lt;span class="s"&gt;"evenodd"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk5jmp5s8am9fqkrhciz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk5jmp5s8am9fqkrhciz.gif" alt="A screen recording of a user of a web application adding and removing todo items from a list."&gt;&lt;/a&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&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&gt;todos-container&lt;/code&gt; frame:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb17j4ii7iejlxia5s2e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb17j4ii7iejlxia5s2e.gif" alt="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."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Neat!&lt;/p&gt;

&lt;h2&gt;
  
  
  Degrading gracefully
&lt;/h2&gt;

&lt;p&gt;Throughout this application, we use &lt;code&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&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F557bs66iljakcmbq721q.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F557bs66iljakcmbq721q.gif" alt="A screen recording of a user of a web application disabling JavaScript in their browser and then using the todo application without any issues"&gt;&lt;/a&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&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="https://turbo.hotwired.dev/reference/drive" rel="noopener noreferrer"&gt;reference documentation&lt;/a&gt; is a good starting point. In particular, familiarizing yourself with the &lt;a href="https://turbo.hotwired.dev/reference/events" rel="noopener noreferrer"&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="https://turbo.hotwired.dev/reference/frames#eager-loaded-frame" rel="noopener noreferrer"&gt;eager&lt;/a&gt; and &lt;a href="https://turbo.hotwired.dev/reference/frames#lazy-loaded-frame" rel="noopener noreferrer"&gt;lazy loaded&lt;/a&gt; frames, &lt;a href="https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets" rel="noopener noreferrer"&gt;breaking out&lt;/a&gt; of frames, and &lt;a href="https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets" rel="noopener noreferrer"&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="https://www.colby.so/posts/turbo-streams-on-rails" rel="noopener noreferrer"&gt;Turbo Streams&lt;/a&gt; and &lt;a href="https://www.colby.so/posts/turbo-frames-on-rails" rel="noopener noreferrer"&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="https://twitter.com/alexandre_ruban" rel="noopener noreferrer"&gt;Alexandre Ruban’s&lt;/a&gt; (in-progress) &lt;a href="https://www.hotrails.dev/" rel="noopener noreferrer"&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;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>turbo</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Pagination and infinite scrolling with Rails and the Hotwire stack</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Fri, 04 Feb 2022 22:24:28 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/pagination-and-infinite-scrolling-with-rails-and-the-hotwire-stack-34om</link>
      <guid>https://dev.to/davidcolbyatx/pagination-and-infinite-scrolling-with-rails-and-the-hotwire-stack-34om</guid>
      <description>&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="https://hotwired.dev/" rel="noopener noreferrer"&gt;Hotwire&lt;/a&gt; stack (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="https://dev.to/dalezak/load-more-pagination-in-rails-with-hotwire-turbo-streams-433e"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxetgod2jrlyynr9vkqkm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxetgod2jrlyynr9vkqkm.gif" alt="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."&gt;&lt;/a&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="https://github.com/DavidColby/turbo-pagination" rel="noopener noreferrer"&gt;on Github&lt;/a&gt;, and you can try out a “production” version of the application &lt;a href="https://guarded-lake-91167.herokuapp.com/" rel="noopener noreferrer"&gt;on Heroku&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s get started!&lt;/p&gt;

&lt;h2&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails new turbo-pagination --css=tailwind
cd turbo-pagination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To demonstrate pagination, we will create a simple &lt;code&gt;Widget&lt;/code&gt; resource. From your terminal again, use the built-in scaffold generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g scaffold Widget name:string
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because we are using Tailwind via the &lt;a href="https://github.com/rails/tailwindcss-rails" rel="noopener noreferrer"&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&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&gt;Widgets&lt;/code&gt; in the database. Open your rails console with &lt;code&gt;rails c&lt;/code&gt; and add test data to the &lt;code&gt;Widgets&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"Widget #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&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="https://github.com/ddnexus/pagy" rel="noopener noreferrer"&gt;Pagy&lt;/a&gt; to implement pagination. Let’s install Pagy now, following along with the Pagy &lt;a href="https://ddnexus.github.io/pagy/how-to#quick-start&amp;amp;gsc.tab=0" rel="noopener noreferrer"&gt;quick start guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;From your terminal, add pagy to your &lt;code&gt;Gemfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle add pagy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add Pagy’s backend to &lt;code&gt;app/controllers/application_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Pagy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Backend&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add Pagy’s frontend helpers to &lt;code&gt;app/helpers/application_helper.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ApplicationHelper&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Pagy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Frontend&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Pagy installed and ready to use across the application, update &lt;code&gt;app/controllers/widgets_controller.rb&lt;/code&gt; to paginate records on the index page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="vi"&gt;@pagy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@widgets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;items: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then finish up our traditional pagination implementation by adding a simple pager UI to &lt;code&gt;app/views/widgets/index.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Widgets&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New widget'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_widget_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"widgets"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@widgets&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"pager"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full my-8 flex justify-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prev&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt; Previous page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;widgets_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="vi"&gt;@pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Next page &amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;widgets_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="vi"&gt;@pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we added the &lt;code&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&gt;prev&lt;/code&gt; and &lt;code&gt;next&lt;/code&gt; methods used on the links are supplied by &lt;code&gt;pagy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With this change in place, boot up the rails application with &lt;code&gt;bin/dev&lt;/code&gt; and head to &lt;a href="http://localhost:3000/widgets" rel="noopener noreferrer"&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;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&gt;
  
  
  Navigate pages with Turbo
&lt;/h2&gt;

&lt;p&gt;In this section, we will use a &lt;a href="https://turbo.hotwired.dev/handbook/frames" rel="noopener noreferrer"&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&gt;app/views/widgets/index.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Widgets&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New widget'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_widget_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"widgets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"min-w-full"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@widgets&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"pager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;pagy: &lt;/span&gt;&lt;span class="vi"&gt;@pagy&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we replaced the &lt;code&gt;widgets&lt;/code&gt; div with a &lt;code&gt;turbo_frame_tag&lt;/code&gt;, and moved the &lt;code&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&gt;widgets&lt;/code&gt; Turbo Frame will now expect to receive a matching &lt;code&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&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&gt;index&lt;/code&gt; page readable.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch app/views/widgets/_pager.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then move the paging controls into the new &lt;code&gt;pager&lt;/code&gt; partial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"pager"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full my-8 flex justify-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prev&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"&amp;lt; Previous page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;widgets_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;turbo_action: &lt;/span&gt;&lt;span class="s2"&gt;"advance"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"Next page &amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;widgets_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;turbo_action: &lt;/span&gt;&lt;span class="s2"&gt;"advance"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The content here is nearly identical to what was previously in the &lt;code&gt;index&lt;/code&gt; page. The only change is the addition of a new &lt;code&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="https://turbo.hotwired.dev/reference/frames#frame-that-promotes-navigations-to-visits" rel="noopener noreferrer"&gt;advance value&lt;/a&gt; for the &lt;code&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="http://localhost:3000/widgets" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpkdqehkyvhhh6cpq5890.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpkdqehkyvhhh6cpq5890.gif" alt="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."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can fix this issue by updating the &lt;code&gt;widgets&lt;/code&gt; Turbo Frame in the widgets index view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"widgets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"min-w-full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;autoscroll: &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://turbo.hotwired.dev/reference/frames#html-attributes" rel="noopener noreferrer"&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&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="https://turbo.hotwired.dev/handbook/streams" rel="noopener noreferrer"&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="https://turbo.hotwired.dev/handbook/streams#stream-messages-and-actions" rel="noopener noreferrer"&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="https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses" rel="noopener noreferrer"&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="https://dev.to/dalezak/load-more-pagination-in-rails-with-hotwire-turbo-streams-433e"&gt;Dale’s article&lt;/a&gt;. In it, a Stimulus controller and &lt;a href="https://github.com/rails/request.js" rel="noopener noreferrer"&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&gt;app/views/widgets/index.html.erb&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Widgets&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New widget'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_widget_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"page_handler"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"widgets"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@widgets&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"pager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;pagy: &lt;/span&gt;&lt;span class="vi"&gt;@pagy&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we added a &lt;code&gt;page_handler&lt;/code&gt; Turbo Frame with no content inside and we removed the &lt;code&gt;widgets&lt;/code&gt; Turbo Frame, which we no longer need.&lt;/p&gt;

&lt;p&gt;This empty &lt;code&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&gt;pager&lt;/code&gt; partial to remove the old pagination controls and replace them with a single load more link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"pager"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full my-8 flex justify-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"Load more widgets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;widgets_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;turbo_frame: &lt;/span&gt;&lt;span class="s2"&gt;"page_handler"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the load more link is targeting the &lt;code&gt;page_handler&lt;/code&gt; Turbo Frame, &lt;a href="https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets" rel="noopener noreferrer"&gt;informing Turbo&lt;/a&gt; that clicks on that link should replace the content of the &lt;code&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&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&gt;app/views/widgets/index.html.erb&lt;/code&gt; with an empty &lt;code&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&gt;turbo_frame&lt;/code&gt; variants, so that we can render different content from the &lt;code&gt;index&lt;/code&gt; action in response to a Turbo Frame request.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;app/controllers/application_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Pagy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Backend&lt;/span&gt;

  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:turbo_frame_request_variant&lt;/span&gt;

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

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;turbo_frame_request_variant&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:turbo_frame&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_request?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are using a turbo-rails method, &lt;code&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&gt;turbo_frame&lt;/code&gt; &lt;a href="https://guides.rubyonrails.org/layouts_and_rendering.html#the-variants-option" rel="noopener noreferrer"&gt;variant&lt;/a&gt; instead of the normal &lt;code&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&gt;index&lt;/code&gt; action. From your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch app/views/widgets/index.html+turbo_frame.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then fill the new view in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"page_handler"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_action_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"append"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"widgets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;template: &lt;/span&gt;&lt;span class="sx"&gt;%(&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@widgets&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sx"&gt;)&lt;/span&gt; 
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_action_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"pager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;template: &lt;/span&gt;&lt;span class="sx"&gt;%(&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"pager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;pagy: &lt;/span&gt;&lt;span class="vi"&gt;@pagy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sx"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we respond with a &lt;code&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="https://github.com/hotwired/turbo-rails/blob/2a2a5ec12e4233cf33e5d3f0a6855834f4e5e4ec/app/helpers/turbo/streams/action_helper.rb#L12" rel="noopener noreferrer"&gt;Turbo Stream&lt;/a&gt; that appends the &lt;code&gt;@widgets&lt;/code&gt; to the existing list of widgets (using the &lt;code&gt;widgets&lt;/code&gt; id). Then we render another Turbo Stream to replace the content of the &lt;code&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&gt;/widgets&lt;/code&gt;, Rails sees the &lt;code&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&gt;page_handler&lt;/code&gt; Turbo Frame tag with the two &lt;code&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkto1iuudulndwzsgndrh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkto1iuudulndwzsgndrh.gif" alt="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."&gt;&lt;/a&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="https://discuss.hotwired.dev/t/tip-turbo-stream-with-http-responses-including-get/3393" rel="noopener noreferrer"&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&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="https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver" rel="noopener noreferrer"&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="https://github.com/stimulus-use/stimulus-use" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/importmap pin stimulus-use
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g stimulus autoclick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fill in the new Stimulus controller at &lt;code&gt;app/javascript/controllers/autoclick_controller.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useIntersection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stimulus-use&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;useIntersection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;appear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This controller pulls in the &lt;a href="https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-intersection.md" rel="noopener noreferrer"&gt;useIntersection&lt;/a&gt; from stimulus-use. The &lt;code&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&gt;appear&lt;/code&gt; simply calls &lt;code&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&gt;pager&lt;/code&gt; partial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"pager"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full my-8 flex justify-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"Load more widgets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;widgets_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;turbo_frame: &lt;/span&gt;&lt;span class="s2"&gt;"page_handler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;controller: &lt;/span&gt;&lt;span class="s2"&gt;"autoclick"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we added &lt;code&gt;data-controller="autoclick"&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&gt;index&lt;/code&gt; action is fired to fetch and append the next set of widgets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkw2axkr5jpxcrthe3ike.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkw2axkr5jpxcrthe3ike.gif" alt="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."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;autoclick&lt;/code&gt; controller we are using here was lightly adapted from Sean Doyle’s &lt;a href="https://github.com/thoughtbot/hotwire-example-template/blob/ed84f0aa74481fbd116f0e66a4801417c9acb40e/app/javascript/controllers/autoclick_controller.js" rel="noopener noreferrer"&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="https://github.com/hotwired/turbo/pull/146" rel="noopener noreferrer"&gt;actions&lt;/a&gt;” to Turbo Frames.&lt;/p&gt;

&lt;p&gt;That's all for this tutorial, great work following along!&lt;/p&gt;

&lt;h2&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="https://discord.gg/stimulus-reflex" rel="noopener noreferrer"&gt;StimulusReflex discord&lt;/a&gt; inspired this article, and the excellent work done by &lt;a href="https://twitter.com/dalezak" rel="noopener noreferrer"&gt;Dale Zak&lt;/a&gt; and &lt;a href="https://github.com/seanpdoyle" rel="noopener noreferrer"&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;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>turbo</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Real-time previews with Rails and StimulusReflex</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Fri, 04 Feb 2022 00:10:51 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/real-time-previews-with-rails-and-stimulusreflex-3p0o</link>
      <guid>https://dev.to/davidcolbyatx/real-time-previews-with-rails-and-stimulusreflex-3p0o</guid>
      <description>&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntqlvqrm3dlz1wnsysb6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntqlvqrm3dlz1wnsysb6.gif" alt="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."&gt;&lt;/a&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="https://post-previewer-md.herokuapp.com/" rel="noopener noreferrer"&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;/p&gt;

&lt;h2&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="https://github.com/DavidColby/stimulus_reflex_previews" rel="noopener noreferrer"&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="https://github.com/DavidColby/stimulus_reflex_previews/pull/1" rel="noopener noreferrer"&gt;this pull request&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our application will allow users to create and edit &lt;code&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g scaffold Post title:string body:text
rails db:migrate
&lt;/code&gt;&lt;/pre&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&gt;bin/dev&lt;/code&gt;. Head to &lt;a href="http://localhost:3000/posts" rel="noopener noreferrer"&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&gt;preview&lt;/code&gt; partial from your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;app/views/posts/_preview.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fill the new partial in with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-200 p-4 shadow-sm rounded-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"prose"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;app/views/posts/new.html.erb&lt;/code&gt; to render the &lt;code&gt;preview&lt;/code&gt; partial beside the post form, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto md:w-2/3 w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex space-x-12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now update &lt;code&gt;app/views/posts/edit.html.erb&lt;/code&gt; with the same content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto md:w-2/3 w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex space-x-12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we could have created a new partial with the content we added to &lt;code&gt;new&lt;/code&gt; and &lt;code&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="http://localhost:3000/posts/new" rel="noopener noreferrer"&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&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&gt;stimulus_reflex&lt;/code&gt; generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g stimulus_reflex post
rails stimulus:manifest:update
&lt;/code&gt;&lt;/pre&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&gt;app/javascript/controllers/post_controller.js&lt;/code&gt; and &lt;code&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&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&gt;app/javascript/controllers/post_controller.js&lt;/code&gt; and update it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ApplicationController&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./application_controller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;ApplicationController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;titlePreview&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bodyPreview&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;titlePreviewTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;titleTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bodyPreviewTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bodyTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we inherit from &lt;code&gt;ApplicationController&lt;/code&gt;, the base StimulusReflex controller.&lt;/p&gt;

&lt;p&gt;In the controller, we define &lt;a href="https://stimulus.hotwired.dev/reference/targets" rel="noopener noreferrer"&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&gt;targets&lt;/code&gt; in the &lt;code&gt;preview&lt;/code&gt; function. Each time &lt;code&gt;preview&lt;/code&gt; is called, the &lt;code&gt;titlePreview&lt;/code&gt; and &lt;code&gt;bodyPreview&lt;/code&gt; elements are updated with the current value of &lt;code&gt;titleTarget&lt;/code&gt; and &lt;code&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&gt;targets&lt;/code&gt; and instead select each element by an id, with something like &lt;code&gt;document.getElementById('body-target')&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&gt;preview&lt;/code&gt; function, we need to connect the new &lt;code&gt;PostController&lt;/code&gt; to the DOM and add &lt;code&gt;target&lt;/code&gt; elements and &lt;code&gt;actions&lt;/code&gt; to the preview and form partials.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;new.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto md:w-2/3 w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex space-x-12"&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"post-preview"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we added &lt;code&gt;data-controller="post"&lt;/code&gt; to the div wrapping the &lt;code&gt;form&lt;/code&gt; and &lt;code&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&gt;data-controller&lt;/code&gt; attribute. Stimulus &lt;a href="https://stimulus.hotwired.dev/reference/controllers#scopes" rel="noopener noreferrer"&gt;scopes controllers&lt;/a&gt; based on the DOM hierarchy — the element with the &lt;code&gt;data-controller&lt;/code&gt; attribute and all of the children of that element will be within that controller’s scope. &lt;code&gt;targets&lt;/code&gt; and &lt;code&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&gt;form&lt;/code&gt; partial next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"contents"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"error_explanation"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;pluralize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; prohibited this post from being saved:&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_message&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;post_target: &lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"input-&amp;gt;post#preview"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_area&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;rows: &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;post_target: &lt;/span&gt;&lt;span class="s2"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"input-&amp;gt;post#preview"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important updates here are on the two form inputs. On each, we added a &lt;code&gt;data-post-target&lt;/code&gt; and a &lt;code&gt;data-action&lt;/code&gt; attribute. The &lt;code&gt;post-target&lt;/code&gt; attribute defines a target for the &lt;code&gt;PostController&lt;/code&gt;, and the &lt;code&gt;action&lt;/code&gt; attribute tells Stimulus to fire the &lt;code&gt;PostController's&lt;/code&gt; preview function when an &lt;code&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&gt;title&lt;/code&gt; or &lt;code&gt;body&lt;/code&gt; inputs, the &lt;code&gt;PostController&lt;/code&gt; will run the &lt;code&gt;preview&lt;/code&gt; function to update the &lt;code&gt;titlePreview&lt;/code&gt; and &lt;code&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&gt;titlePreview&lt;/code&gt; and &lt;code&gt;bodyPreview&lt;/code&gt; targets. Head to &lt;code&gt;app/views/posts/_preview.html.erb&lt;/code&gt; and update it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-200 p-4 shadow-sm rounded-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"prose"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold"&lt;/span&gt; &lt;span class="na"&gt;data-post-target=&lt;/span&gt;&lt;span class="s"&gt;"titlePreview"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-post-target=&lt;/span&gt;&lt;span class="s"&gt;"bodyPreview"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fux0eqvy22b8f7kk5sr8r.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fux0eqvy22b8f7kk5sr8r.gif" alt="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."&gt;&lt;/a&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&gt;PostReflex&lt;/code&gt; that we generated at the beginning of this section.&lt;/p&gt;

&lt;h2&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&gt;PostReflex&lt;/code&gt;, at &lt;code&gt;app/reflexes/post_reflex.rb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This &lt;a href="https://docs.stimulusreflex.com/rtfm/reflex-classes" rel="noopener noreferrer"&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&gt;PostReflex&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostReflex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationReflex&lt;/span&gt;  
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;preview&lt;/span&gt;
    &lt;span class="n"&gt;morph&lt;/span&gt; &lt;span class="s1"&gt;'#preview-title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;morph&lt;/span&gt; &lt;span class="s1"&gt;'#preview-body'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

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

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_params&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this reflex, we are using two &lt;a href="https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs" rel="noopener noreferrer"&gt;selector morphs&lt;/a&gt; to update the preview content, identifying the element to update with an &lt;code&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&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="https://docs.stimulusreflex.com/rtfm/reflexes" rel="noopener noreferrer"&gt;call a server-side reflex&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first option is through a &lt;code&gt;reflex&lt;/code&gt; data attribute on a DOM element.  &lt;a href="https://docs.stimulusreflex.com/rtfm/reflexes#declaring-a-reflex-in-html-with-data-attributes" rel="noopener noreferrer"&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="https://docs.stimulusreflex.com/rtfm/reflexes#calling-a-reflex-in-a-stimulus-controller" rel="noopener noreferrer"&gt;this.stimulate&lt;/a&gt; in a Stimulus controller. &lt;code&gt;stimulate&lt;/code&gt; is &lt;a href="https://docs.stimulusreflex.com/rtfm/reflexes#stimulate-is-extremely-flexible" rel="noopener noreferrer"&gt;extremely flexible&lt;/a&gt; and allows us to override the &lt;code&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&gt;stimulate&lt;/code&gt;, head back to &lt;code&gt;app/javascript/controllers/post_controller.js&lt;/code&gt; and update it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ApplicationController&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./application_controller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;ApplicationController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stimulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Post#preview&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;serializeForm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we removed the &lt;code&gt;targets&lt;/code&gt; from the controller and &lt;code&gt;preview&lt;/code&gt; now just calls &lt;code&gt;this.stimulate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;serializeForm&lt;/code&gt; &lt;a href="https://docs.stimulusreflex.com/v/v3.5/rtfm/working-with-forms#stimulusreflex-form-processing" rel="noopener noreferrer"&gt;option&lt;/a&gt; ensures the content of the closest &lt;code&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&gt;params&lt;/code&gt;, as we saw in the &lt;code&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&gt;preview&lt;/code&gt; method in &lt;code&gt;PostReflex&lt;/code&gt;, we have two &lt;code&gt;morphs&lt;/code&gt; that rely on ids to target the right element in the DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;preview&lt;/span&gt;
  &lt;span class="n"&gt;morph&lt;/span&gt; &lt;span class="s1"&gt;'#preview-title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;morph&lt;/span&gt; &lt;span class="s1"&gt;'#preview-body'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the reflex to work, we need an element with a &lt;code&gt;preview-title&lt;/code&gt; id and another with a &lt;code&gt;preview-body&lt;/code&gt; id. Update the preview partial like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-200 p-4 shadow-sm rounded-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"prose"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"preview-title"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"preview-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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;Move over to the &lt;code&gt;form&lt;/code&gt; partial to connect the &lt;code&gt;post&lt;/code&gt; Stimulus controller to the form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"contents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;controller: &lt;/span&gt;&lt;span class="s2"&gt;"post"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"error_explanation"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;pluralize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; prohibited this post from being saved:&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_message&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"input-&amp;gt;post#preview"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_area&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;rows: &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"input-&amp;gt;post#preview"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, notice the &lt;code&gt;form&lt;/code&gt; element now has a &lt;code&gt;data-controller="post"&lt;/code&gt; attribute, scoping the Stimulus controller to the form. We also updated the form inputs to remove the unnecessary &lt;code&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&gt;PostController&lt;/code&gt; to the form, we need to remove the controller from the wrapper div in &lt;code&gt;app/view/posts/new.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto md:w-2/3 w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex space-x-12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&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&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="http://localhost:3000/posts/new" rel="noopener noreferrer"&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;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jlupr8203av9bx9t518.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jlupr8203av9bx9t518.gif" alt="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."&gt;&lt;/a&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&gt;
  
  
  Add markdown and liquid parsing
&lt;/h2&gt;

&lt;p&gt;We will use &lt;a href="https://github.com/vmg/redcarpet" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add redcarpet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart your Rails application and head to &lt;code&gt;app/helpers/posts_helper.rb&lt;/code&gt; and add a method to parse markdown content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;PostsHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

    &lt;span class="n"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Redcarpet&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Redcarpet&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Render&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;fenced_code_blocks: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;autolink: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helper takes a string, initializes a new instance of &lt;code&gt;Redcarpet::Markdown&lt;/code&gt; as described in the &lt;a href="https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, and then renders the content to HTML.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;to_markdown&lt;/code&gt; helper will be used in two places in our application: When the &lt;code&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&gt;PostReflex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Update the &lt;code&gt;preview&lt;/code&gt; partial first, at &lt;code&gt;app/views/posts/_preview.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-200 p-4 shadow-sm rounded-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"prose"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"preview-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"preview-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user visits the &lt;code&gt;new&lt;/code&gt; or &lt;code&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&gt;PostReflex&lt;/code&gt; to use the new &lt;code&gt;to_markdown&lt;/code&gt; helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;preview&lt;/span&gt;
  &lt;span class="n"&gt;morph&lt;/span&gt; &lt;span class="s1"&gt;'#preview-title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;morph&lt;/span&gt; &lt;span class="s1"&gt;'#preview-body'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Head to &lt;code&gt;new&lt;/code&gt; or &lt;code&gt;edit&lt;/code&gt; now, type some markdown content in the &lt;code&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkg5bo00yutbnlgzb1uxg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkg5bo00yutbnlgzb1uxg.gif" alt="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."&gt;&lt;/a&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="https://github.com/Shopify/liquid" rel="noopener noreferrer"&gt;liquid&lt;/a&gt; support, allowing users to transform &lt;a href="https://shopify.github.io/liquid/basics/introduction/" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add liquid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back to the &lt;code&gt;PostsHelper&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;PostsHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

    &lt;span class="n"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Redcarpet&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Redcarpet&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Render&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;fenced_code_blocks: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;autolink: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;liquified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;liquified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Liquid&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Template&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'company_name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Hotwiring Rails'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;liquified&lt;/code&gt; takes a string of &lt;code&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&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&gt;new&lt;/code&gt; or &lt;code&gt;edit&lt;/code&gt;posts page, enter in a body with some markdown content and the &lt;code&gt;{{ company_name }}&lt;/code&gt; tag and see that the tag is replaced as you type:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fufiw01zlrs7pddetpbi8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fufiw01zlrs7pddetpbi8.gif" alt="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."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&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&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&gt;preview&lt;/code&gt; function, we will use lodash’s &lt;code&gt;debounce&lt;/code&gt;. From your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add lodash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then update the &lt;code&gt;Post&lt;/code&gt; Stimulus controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ApplicationController&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./application_controller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;debounce&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lodash/debounce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;ApplicationController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stimulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Post#preview&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;serializeForm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, &lt;code&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&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&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="https://github.com/markedjs/marked" rel="noopener noreferrer"&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&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&gt;
  
  
  Further reading
&lt;/h3&gt;

&lt;p&gt;The best resources for learning about Stimulus are the official &lt;a href="https://stimulus.hotwired.dev/handbook/introduction" rel="noopener noreferrer"&gt;handbook&lt;/a&gt; and &lt;a href="https://stimulus.hotwired.dev/reference/controllers" rel="noopener noreferrer"&gt;reference&lt;/a&gt;. The official documentation does not cover more advanced use cases and best practices. For that, &lt;a href="https://www.betterstimulus.com/" rel="noopener noreferrer"&gt;BetterStimulus&lt;/a&gt; is a great starting point.&lt;/p&gt;

&lt;p&gt;To learn more about StimulusReflex, &lt;a href="https://docs.stimulusreflex.com/" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; is exceptional, and is the best place to start your journey. The &lt;a href="https://discord.gg/stimulus-reflex" rel="noopener noreferrer"&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;

&lt;p&gt;PS: If you enjoyed this post, you might enjoy my monthly newsletter on the latest in modern Rails development, &lt;a href="https://www.getrevue.co/profile/hotwiringrails" rel="noopener noreferrer"&gt;Hotwiring Rails&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>stimulus</category>
      <category>stimulusreflex</category>
    </item>
    <item>
      <title>Live reloading with Ruby on Rails and esbuild</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Sat, 06 Nov 2021 16:24:00 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/live-reloading-with-ruby-on-rails-and-esbuild-4cdd</link>
      <guid>https://dev.to/davidcolbyatx/live-reloading-with-ruby-on-rails-and-esbuild-4cdd</guid>
      <description>&lt;p&gt;As you may have heard by now, Rails 7 comes out of the box with &lt;a href="https://github.com/rails/importmap-rails" rel="noopener noreferrer"&gt;importmap-rails&lt;/a&gt; and the mighty &lt;a href="https://github.com/rails/webpacker" rel="noopener noreferrer"&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="https://github.com/rails/jsbundling-rails" rel="noopener noreferrer"&gt;jsbundling-rails&lt;/a&gt; was created. This gem adds the option to use &lt;a href="https://webpack.js.org/" rel="noopener noreferrer"&gt;webpack&lt;/a&gt;, &lt;a href="https://rollupjs.org/guide/en/" rel="noopener noreferrer"&gt;rollup&lt;/a&gt;, or &lt;a href="https://esbuild.github.io/" rel="noopener noreferrer"&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="https://webpack.js.org/concepts/hot-module-replacement/" rel="noopener noreferrer"&gt;Hot Module Replacement&lt;/a&gt;), losing it is tough.&lt;/p&gt;

&lt;p&gt;Today, esbuild &lt;a href="https://github.com/evanw/esbuild/issues/645#issuecomment-755215007" rel="noopener noreferrer"&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;/p&gt;

&lt;h2&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="https://www.aloucaslabs.com/miniposts/using-a-specific-rails-version-when-you-generate-a-new-rails-app-with-rails-new-command" rel="noopener noreferrer"&gt;this article&lt;/a&gt; can help you get your local environment ready.&lt;/p&gt;

&lt;p&gt;Once your &lt;code&gt;rails new&lt;/code&gt; command is ready for Rails 7, from your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails new live_esbuild -j esbuild
cd live_esbuild
rails db:create
rails g controller Home index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we created a new Rails application set to use &lt;code&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&gt;
  
  
  Booting up
&lt;/h3&gt;

&lt;p&gt;In addition to installing esbuild for us, &lt;code&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&gt;rails s&lt;/code&gt;, you’ll use &lt;code&gt;bin/dev&lt;/code&gt;. &lt;code&gt;bin/dev&lt;/code&gt; uses &lt;a href="https://github.com/ddollar/foreman" rel="noopener noreferrer"&gt;foreman&lt;/a&gt; to run multiple start up scripts, via &lt;code&gt;Procfile.dev&lt;/code&gt;. We’ll make a change to the &lt;code&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&gt;bin/dev&lt;/code&gt; to make sure your assets are built properly.&lt;/p&gt;

&lt;h2&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch esbuild-dev.config.js
&lt;/code&gt;&lt;/pre&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&gt;esbuild-dev.config.js&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--watch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watchOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;onRebuild&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Build failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Build succeeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data: update&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/assets/builds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;absWorkingDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;watchOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;banner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; (() =&amp;gt; new EventSource("http://localhost:8082").onmessage = () =&amp;gt; location.reload())();&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8082&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--watch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;clients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;watchOptions&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watchOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;onRebuild&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Build failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Build succeeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data: update&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&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&gt;res.write&lt;/code&gt; to send data out to each client.&lt;/p&gt;

&lt;p&gt;Finally, &lt;code&gt;clients.length = 0&lt;/code&gt; empties the &lt;code&gt;clients&lt;/code&gt; array to prepare it for the next rebuild.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/assets/builds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;absWorkingDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;watchOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;banner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; (() =&amp;gt; new EventSource("http://localhost:8082").onmessage = () =&amp;gt; location.reload())();&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This section defines the esbuild &lt;code&gt;build&lt;/code&gt; command, passing in &lt;a href="https://esbuild.github.io/api/#build-api" rel="noopener noreferrer"&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="https://esbuild.github.io/api/#watch" rel="noopener noreferrer"&gt;watch option&lt;/a&gt;, which takes the &lt;code&gt;watch&lt;/code&gt; and &lt;code&gt;watchOptions&lt;/code&gt; variables we defined earlier and &lt;code&gt;banner&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;esbuild’s &lt;a href="https://esbuild.github.io/api/#banner" rel="noopener noreferrer"&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="https://developer.mozilla.org/en-US/docs/Web/API/EventSource" rel="noopener noreferrer"&gt;EventSource&lt;/a&gt; that fires &lt;code&gt;location.reload()&lt;/code&gt; each time a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource/onmessage" rel="noopener noreferrer"&gt;message is received&lt;/a&gt; from &lt;code&gt;localhost:8082&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Inserting the &lt;code&gt;EventSource&lt;/code&gt; banner and sending a new request from &lt;code&gt;8082&lt;/code&gt; each time &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8082&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;http&lt;/code&gt; &lt;a href="https://nodejs.dev/learn/the-nodejs-http-module" rel="noopener noreferrer"&gt;module&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With the esbuild file updated, we need to update &lt;code&gt;package.json&lt;/code&gt; to use the new config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"esbuild app/javascript/*.* --bundle --outdir=app/assets/builds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node esbuild-dev.config.js"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we updated the &lt;code&gt;scripts&lt;/code&gt;  section of &lt;code&gt;package.json&lt;/code&gt; to add a new &lt;code&gt;start&lt;/code&gt; script that uses our new config file. We’ve left &lt;code&gt;build&lt;/code&gt; as-is since &lt;code&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&gt;Procfile.dev&lt;/code&gt; to use the &lt;code&gt;start&lt;/code&gt; script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web: bin/rails server -p 3000
js: yarn start --watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, let’s make sure our JavaScript reloading works. Update &lt;code&gt;app/views/home/index.html.erb&lt;/code&gt; to connect the default &lt;code&gt;hello&lt;/code&gt; Stimulus controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Home#index&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Find me in app/views/home/index.html.erb&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now boot up the app with &lt;code&gt;bin/dev&lt;/code&gt; and head to &lt;a href="http://localhost:3000/home/index" rel="noopener noreferrer"&gt;http://localhost:3000/home/index&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then open up &lt;code&gt;app/javascript/hello_controller.js&lt;/code&gt; and make a change to the &lt;code&gt;connect&lt;/code&gt; method, maybe something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello Peter. What's happening?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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="https://github.com/paulmillr/chokidar" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add chokidar -D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With chokidar installed, we’ll update &lt;code&gt;esbuild-dev.config.js&lt;/code&gt; again, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chokidar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chokidar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8082&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/assets/builds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;absWorkingDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;incremental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;banner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; (() =&amp;gt; new EventSource("http://localhost:8082").onmessage = () =&amp;gt; location.reload())();&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;chokidar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./app/javascript/**/*.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./app/views/**/*.html.erb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./app/assets/stylesheets/*.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rebuild&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data: update&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, lots going on here. Let’s step through the important bits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chokidar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chokidar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we require &lt;code&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&gt;build&lt;/code&gt; task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// snip unchanged options&lt;/span&gt;
    &lt;span class="na"&gt;incremental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;chokidar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./app/javascript/**/*.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./app/views/**/*.html.erb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./app/assets/stylesheets/*.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rebuild&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data: update&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’ve moved the &lt;code&gt;build&lt;/code&gt; setup into an async function that assigns &lt;code&gt;result&lt;/code&gt; to &lt;code&gt;build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We also added the &lt;code&gt;incremental&lt;/code&gt; &lt;a href="https://esbuild.github.io/api/#incremental" rel="noopener noreferrer"&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&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&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&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&gt;bin/dev&lt;/code&gt; again. Open up or refresh &lt;a href="http://localhost:3000/home/index" rel="noopener noreferrer"&gt;http://localhost:3000/home/index&lt;/a&gt;, make changes to &lt;code&gt;index.html.erb&lt;/code&gt; and &lt;code&gt;application.css&lt;/code&gt; and see that those changes trigger page reloads and that updating &lt;code&gt;hello_controller.js&lt;/code&gt; still triggers a reload.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa9y5zgkn3xubtwocy43e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa9y5zgkn3xubtwocy43e.gif" alt="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."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&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="https://github.com/DavidColby/esbuild-live-reload" rel="noopener noreferrer"&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="https://github.com/rails/jsbundling-rails/issues/40#issuecomment-939514493" rel="noopener noreferrer"&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="https://github.com/evanw/esbuild/issues/802" rel="noopener noreferrer"&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="https://dev.to/paramagicdev/frontend-bundler-braindump-10fj"&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="https://vite-ruby.netlify.app/" rel="noopener noreferrer"&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="https://github.com/excid3/esbuild-rails" rel="noopener noreferrer"&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;

</description>
      <category>rails</category>
      <category>esbuild</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Rendering view components with Turbo Stream broadcasts</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Thu, 04 Nov 2021 14:32:29 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/rendering-view-components-with-turbo-stream-broadcasts-2hfn</link>
      <guid>https://dev.to/davidcolbyatx/rendering-view-components-with-turbo-stream-broadcasts-2hfn</guid>
      <description>&lt;p&gt;View components, via Github’s &lt;a href="https://github.com/github/view_component" rel="noopener noreferrer"&gt;view_component gem&lt;/a&gt;, are growing in popularity in the Rails community but until recently, view components and &lt;a href="https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb" rel="noopener noreferrer"&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="https://blog.kuda.dev/making-hotwire-play-nice-with-viewcomponent-ckoilssb40msdv9s10v139kgs" rel="noopener noreferrer"&gt;ways&lt;/a&gt; to get components working with streams, thanks to a recent addition to &lt;a href="https://github.com/hotwired/turbo-rails" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbg8mx1q5pnan4gpejvj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbg8mx1q5pnan4gpejvj.gif" alt="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"&gt;&lt;/a&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="https://github.com/DavidColby/turbo-view-components/tree/broadcast-spies" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start building!&lt;/p&gt;

&lt;h2&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="https://github.com/DavidColby/turbo-view-components" rel="noopener noreferrer"&gt;Github repo&lt;/a&gt; and skip ahead to the Building the spy component section.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails new turbo-view-components -T
cd 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;/div&gt;



&lt;p&gt;If you’re using Rails 6.1 instead of 7, you’ll also need to install &lt;code&gt;turbo-rails&lt;/code&gt; manually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle add turbo-rails
rails turbo:install
&lt;/code&gt;&lt;/pre&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&gt;view_component&lt;/code&gt; and &lt;code&gt;turbo-rails&lt;/code&gt; installed in your application and a &lt;code&gt;Spy&lt;/code&gt; resource created. Once you're setup, start up your server with &lt;code&gt;rails s&lt;/code&gt; and head to &lt;a href="http://localhost:3000/spies" rel="noopener noreferrer"&gt;http://localhost:3000/spies&lt;/a&gt;.&lt;/p&gt;

&lt;h2&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="https://viewcomponent.org/guide/generators.html" rel="noopener noreferrer"&gt;generator&lt;/a&gt; &lt;code&gt;view_component&lt;/code&gt; provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g component Spy spy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generator will create both a &lt;code&gt;spy_component.rb&lt;/code&gt; file and a &lt;code&gt;spy_component.html.erb&lt;/code&gt; file in the &lt;code&gt;components&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Since we're building a very simple component, &lt;code&gt;spy_component.rb&lt;/code&gt; is good to go out of the box. For reference, it should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpyComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spy&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@spy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spy&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt; &lt;span class="vi"&gt;@spy&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Name:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@spy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Mission:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@spy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mission&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Show this spy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@spy&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is just regular old &lt;code&gt;erb&lt;/code&gt;, essentially a copy of the default content of &lt;code&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&gt;spies/index.html.erb&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Spy&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"spies"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SpyComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@spies&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"New spy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_spy_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’re using &lt;a href="https://viewcomponent.org/guide/collections.html" rel="noopener noreferrer"&gt;collection rendering&lt;/a&gt; to loop through each &lt;code&gt;spy&lt;/code&gt; in &lt;code&gt;@spies&lt;/code&gt; and render each with &lt;code&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&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&gt;turbo_stream_from&lt;/code&gt; helper from turbo-rails. Update &lt;code&gt;spies/index.html.erb&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Snip --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="s2"&gt;"spies"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"spies"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SpyComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@spies&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Snip --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two important pieces here. First, we added the &lt;code&gt;turbo_stream_from&lt;/code&gt; helper, with &lt;code&gt;spies&lt;/code&gt; as the name.&lt;/p&gt;

&lt;p&gt;This helper creates a &lt;code&gt;&amp;lt;turbo-cable-stream-source&amp;gt;&lt;/code&gt; in the rendered HTML with a &lt;a href="https://github.com/hotwired/turbo-rails/blob/a0f14b9e4649f60bcf8fa8b82f35e7f460d60177/app/channels/turbo/streams/stream_name.rb" rel="noopener noreferrer"&gt;signed-stream-name&lt;/a&gt; that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-cable-stream-source&lt;/span&gt; &lt;span class="na"&gt;channel=&lt;/span&gt;&lt;span class="s"&gt;"Turbo::StreamsChannel"&lt;/span&gt; &lt;span class="na"&gt;signed-stream-name=&lt;/span&gt;&lt;span class="s"&gt;"aLongSecureName"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/turbo-cable-stream-source&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, the div wrapping the list of spy components has an id of &lt;code&gt;spies&lt;/code&gt;. This id must match the target of the Turbo Stream broadcast, which &lt;a href="https://github.com/hotwired/turbo-rails/blob/ba86832f7f13001793ab917185788df9723666e8/app/models/concerns/turbo/broadcastable.rb#L68" rel="noopener noreferrer"&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&gt;models/spy.rb&lt;/code&gt; to broadcast newly created spies on the &lt;code&gt;spies&lt;/code&gt; channel.&lt;/p&gt;

&lt;p&gt;Update &lt;code&gt;spy.rb&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

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

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

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_new_record&lt;/span&gt;
    &lt;span class="n"&gt;broadcast_append_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s1"&gt;'spies'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="no"&gt;ApplicationController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="no"&gt;SpyComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;spy: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’re using the new &lt;code&gt;html&lt;/code&gt; option added to &lt;a href="https://github.com/hotwired/turbo-rails/pull/245" rel="noopener noreferrer"&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&gt;ApplicationController.render&lt;/code&gt; to render a view_component isn’t officially sanctioned by the &lt;code&gt;view_component&lt;/code&gt; team.&lt;/p&gt;

&lt;p&gt;The &lt;code&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="https://github.com/github/view_component/issues/1106" rel="noopener noreferrer"&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="https://github.com/rails/rails/pull/43427" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;silence_disabled_session_errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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="http://localhost:3000/spies/new" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1khnm0p459g03qo40ww.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1khnm0p459g03qo40ww.gif" alt="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"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Today we looked at a technique to render View Components from Turbo Stream broadcasts, leveraging the recently added ability to render html instead of a partial from 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="https://viewcomponent.org/" rel="noopener noreferrer"&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="https://github.blog/2020-12-15-encapsulating-ruby-on-rails-views/" rel="noopener noreferrer"&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="https://github.com/hotwired/turbo-rails" rel="noopener noreferrer"&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="https://turbo.hotwired.dev/handbook/introduction" rel="noopener noreferrer"&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;

</description>
      <category>rails</category>
      <category>hotwire</category>
      <category>tutorial</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Filter, search, and sort tables with Rails and Turbo Frames</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Fri, 15 Oct 2021 19:34:30 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/filter-search-and-sort-tables-with-rails-and-turbo-frames-1ea2</link>
      <guid>https://dev.to/davidcolbyatx/filter-search-and-sort-tables-with-rails-and-turbo-frames-1ea2</guid>
      <description>&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="https://www.colby.so/posts/sortable-table-with-rails-and-turbo-frames" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fabgabcu9q2wbxwc0dukx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fabgabcu9q2wbxwc0dukx.gif" alt="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"&gt;&lt;/a&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;/p&gt;

&lt;h2&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="https://www.colby.so/posts/sortable-table-with-rails-and-turbo-frames" rel="noopener noreferrer"&gt;sortable table article&lt;/a&gt; locally, you’ll want to begin by cloning &lt;a href="https://github.com/DavidColby/player_sorting_frames" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle install
yarn install
rails db:setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the application is ready, checkout the &lt;a href="https://github.com/DavidColby/player_sorting_frames/tree/sortable" rel="noopener noreferrer"&gt;sortable branch&lt;/a&gt;, where this article picks up, and then run &lt;code&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="http://localhost:3000" rel="noopener noreferrer"&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&gt;
  
  
  Add a search form
&lt;/h2&gt;

&lt;p&gt;We’ll start with a simple search form, added inside the &lt;code&gt;players&lt;/code&gt; turbo frame in  &lt;code&gt;app/views/players/_players.html.erb&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"players"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"shadow overflow-hidden rounded border-b border-gray-200"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-end mb-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;list_players_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :get&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"Search by name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"border border-blue-500 rounded p-2"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt; &lt;span class="s2"&gt;"Search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-500 text-white p-2 rounded-sm"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Snip the table --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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&gt;list&lt;/code&gt; method in &lt;code&gt;app/controllers/players_controller.rb&lt;/code&gt; to filter the list of players when the search form is submitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name ilike ?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:direction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'players'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;players: &lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’ve got a clunky, functional implementation of searching by name — when the &lt;code&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&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&gt;stimulus-rails&lt;/code&gt;. From your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g stimulus search_form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, fill that controller in with:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestSubmit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This controller’s &lt;code&gt;search&lt;/code&gt; function calls &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit" rel="noopener noreferrer"&gt;requestSubmit&lt;/a&gt; on a &lt;code&gt;form&lt;/code&gt; target. Since we’ll call this &lt;code&gt;search&lt;/code&gt; function as the user is typing in a text input, we’ve add &lt;code&gt;clearTimeout&lt;/code&gt; and &lt;code&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&gt;requestSubmit&lt;/code&gt; needs a &lt;a href="https://github.com/javan/form-request-submit-polyfill" rel="noopener noreferrer"&gt;polyfill&lt;/a&gt; for support on Safari and IE11. As an alternative to &lt;code&gt;requestSubmit&lt;/code&gt;, if you are using &lt;code&gt;Rails/ujs&lt;/code&gt; in your application, &lt;code&gt;Rails.fire(this.formTarget, 'submit')&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-end mb-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;list_players_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;controller: &lt;/span&gt;&lt;span class="s2"&gt;"search-form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;search_form_target: &lt;/span&gt;&lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;turbo_frame: &lt;/span&gt;&lt;span class="s2"&gt;"players"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"Search by name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"border border-blue-500 rounded p-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;autocomplete: &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"input-&amp;gt;search-form#search"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"players"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"shadow overflow-hidden rounded border-b border-gray-200"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Snip table --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we first moved the search form outside of the &lt;code&gt;players&lt;/code&gt; &lt;code&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&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F99ptawtf1q9kjuqgp718.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F99ptawtf1q9kjuqgp718.gif" alt="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"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the form is now outside of the frame, we add a &lt;code&gt;data-turbo-frame&lt;/code&gt; to target the &lt;code&gt;players&lt;/code&gt; frame. This &lt;a href="https://turbo.hotwired.dev/handbook/frames#targeting-navigation-into-or-out-of-a-frame" rel="noopener noreferrer"&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&gt;data-controller="search-form"&lt;/code&gt; and &lt;code&gt;data-search_form_target="form"&lt;/code&gt; to the form element. These Stimulus attributes &lt;a href="https://stimulus.hotwired.dev/reference/controllers#identifiers" rel="noopener noreferrer"&gt;connect&lt;/a&gt; the &lt;code&gt;search-form&lt;/code&gt; controller to the DOM and set the &lt;code&gt;form&lt;/code&gt; &lt;a href="https://stimulus.hotwired.dev/reference/targets#attributes-and-names" rel="noopener noreferrer"&gt;target&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, we add &lt;code&gt;data-action=“input-&amp;gt;search-form#search”&lt;/code&gt; to the &lt;code&gt;name&lt;/code&gt; field, which tells Stimulus to call the &lt;code&gt;search&lt;/code&gt; function each time the &lt;code&gt;input&lt;/code&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event" rel="noopener noreferrer"&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="https://stimulus.hotwired.dev/handbook/introduction" rel="noopener noreferrer"&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&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&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&gt;list&lt;/code&gt; has no way of retaining sort and search options across requests — every new request to &lt;code&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&gt;params&lt;/code&gt; to storing filter options in the &lt;code&gt;session&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our basic approach will be to use &lt;code&gt;params&lt;/code&gt; to update a &lt;code&gt;filters&lt;/code&gt; hash in the &lt;code&gt;session&lt;/code&gt; object. Since &lt;code&gt;session&lt;/code&gt; is maintained across requests, as long as we update the session &lt;code&gt;filters&lt;/code&gt; hash with the search and sort &lt;code&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&gt;list&lt;/code&gt; method looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;
  &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filters'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filters'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

  &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filters'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;merge!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'players.name ilike ?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filters'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filters'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filters'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'column'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filters'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'direction'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'players'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;players: &lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_params&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we ensure that &lt;code&gt;session['filters']&lt;/code&gt; is a hash, update its value by merging in whitelisted &lt;code&gt;filter_params&lt;/code&gt; and then use the the &lt;code&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&gt;list&lt;/code&gt; method and add the &lt;code&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&gt;params&lt;/code&gt; in a hash in the &lt;code&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&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&gt;Filterable&lt;/code&gt; &lt;a href="https://api.rubyonrails.org/v6.1.4/classes/ActiveSupport/Concern.html" rel="noopener noreferrer"&gt;concern&lt;/a&gt;. We’ll include &lt;code&gt;Filterable&lt;/code&gt; in the &lt;code&gt;PlayersController&lt;/code&gt; and use it to filter &lt;code&gt;players&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Filterable&lt;/code&gt; won’t know anything about the specifics of querying &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch app/controllers/concerns/filterable.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, fill in &lt;code&gt;filterable.rb&lt;/code&gt; with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Filterable&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;store_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;apply_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

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

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;store_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;underscore&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_filters"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;key?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;underscore&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_filters"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;underscore&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_filters"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;merge!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter_params_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_params_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FILTER_PARAMS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;underscore&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_filters"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s a lot of code here, let’s break it down.&lt;/p&gt;

&lt;p&gt;The &lt;code&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&gt;resource&lt;/code&gt; argument. &lt;code&gt;resource&lt;/code&gt; will be an ActiveRecord class, like &lt;code&gt;Player&lt;/code&gt;. &lt;code&gt;filter!&lt;/code&gt; simply calls out to two internal methods, &lt;code&gt;store_filters&lt;/code&gt; and &lt;code&gt;apply_filters&lt;/code&gt;, which do the heavy lifting.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;store_filters&lt;/code&gt; ensures that &lt;code&gt;session['class_name_filters']&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&gt;list&lt;/code&gt; method in the last section.&lt;/p&gt;

&lt;p&gt;Once the filters are stored, &lt;code&gt;apply_filters&lt;/code&gt; calls a &lt;code&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&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&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&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&gt;Filterable&lt;/code&gt;, we instead just rely on the target class to define &lt;code&gt;FILTER_PARAMS&lt;/code&gt; and a &lt;code&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&gt;app/models/player.rb&lt;/code&gt; to work with our new &lt;code&gt;Filterable&lt;/code&gt; concern.&lt;br&gt;
&lt;/p&gt;

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

  &lt;span class="no"&gt;FILTER_PARAMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%i[name column direction]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:by_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'players.name ilike ?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'column'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'direction'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’ve defined the &lt;code&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&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&gt;filter&lt;/code&gt;, which replaces the queries that we previously built in &lt;code&gt;PlayersController#list&lt;/code&gt; and makes use of the new &lt;code&gt;by_name&lt;/code&gt; scope to be a bit more readable.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;Player&lt;/code&gt; set up for filtering, we can update &lt;code&gt;PlayersController&lt;/code&gt; to include &lt;code&gt;Filterable&lt;/code&gt; and replace the filtering logic in &lt;code&gt;list&lt;/code&gt; with the new &lt;code&gt;filter!&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Filterable&lt;/span&gt;
  &lt;span class="c1"&gt;# snip&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;
    &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'players'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;players: &lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we simply include &lt;code&gt;Filterable&lt;/code&gt; in the controller and then set the value of &lt;code&gt;players&lt;/code&gt; using the &lt;code&gt;filter!&lt;/code&gt; method provided by &lt;code&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&gt;Filterable&lt;/code&gt; is in the &lt;code&gt;controller/concerns&lt;/code&gt; directory, but it isn't truly a &lt;a href="https://api.rubyonrails.org/v6.1.4/classes/ActiveSupport/Concern.html" rel="noopener noreferrer"&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&gt;_players&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-name"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_indicator&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'player_filters'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'column'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;build_order_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-team"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_indicator&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'player_filters'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'column'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"teams.name"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;build_order_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="s2"&gt;"teams.name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Team"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-seasons"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_indicator&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'player_filters'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'column'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"seasons"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;build_order_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="s2"&gt;"seasons"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Seasons"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ve replaced references to &lt;code&gt;params&lt;/code&gt; with &lt;code&gt;session.dig&lt;/code&gt; calls with this change. When no filters have been applied, &lt;code&gt;session['player_filters']&lt;/code&gt; won’t exist, so we use &lt;code&gt;dig&lt;/code&gt; to avoid nil errors in those cases.&lt;/p&gt;

&lt;p&gt;Next, update &lt;code&gt;app/heleprs/players_helper.rb&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;PlayersHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_order_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'player_filters'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'column'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list_players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;direction: &lt;/span&gt;&lt;span class="n"&gt;next_direction&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list_players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;direction: &lt;/span&gt;&lt;span class="s1"&gt;'asc'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;next_direction&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'player_filters'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'direction'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'asc'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'desc'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'asc'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort_indicator&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"sort sort-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'player_filters'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'direction'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&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&gt;team&lt;/code&gt; is a &lt;code&gt;belongs_to&lt;/code&gt; relationship on the &lt;code&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add @tailwindcss/forms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then update &lt;code&gt;tailwind.config.js&lt;/code&gt; to include the plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tailwindcss/forms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;players&lt;/code&gt; partial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;list_players_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;controller: &lt;/span&gt;&lt;span class="s2"&gt;"search-form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;search_form_target: &lt;/span&gt;&lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;turbo_frame: &lt;/span&gt;&lt;span class="s2"&gt;"players"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="ss"&gt;:team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;options_for_select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'player_filters'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'team_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;include_blank: &lt;/span&gt;&lt;span class="s1"&gt;'All Teams'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"border-blue-500 rounded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"change-&amp;gt;search-form#search"&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt; 
  &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Snip the search input --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a regular Rails &lt;code&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&gt;session.dig&lt;/code&gt;, again) and fire the &lt;code&gt;search&lt;/code&gt; function of the &lt;code&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&gt;Player&lt;/code&gt; to support filtering by team yet.&lt;/p&gt;

&lt;p&gt;Head to &lt;code&gt;app/models/player.rb&lt;/code&gt; and update it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;FILTER_PARAMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%i[name team_id column direction]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:by_team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;team_id: &lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_team&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'team_id'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'column'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'direction'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;team_id&lt;/code&gt; to the list of valid &lt;code&gt;FILTER_PARAMS&lt;/code&gt;, added the &lt;code&gt;by_team&lt;/code&gt; scope, and then added &lt;code&gt;by_team&lt;/code&gt; to the &lt;code&gt;filters&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;No need to change our controller or touch the &lt;code&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6b7qgskgktg5814w0bmo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6b7qgskgktg5814w0bmo.gif" alt="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"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&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&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&gt;filter!&lt;/code&gt; method, we can update the &lt;code&gt;index&lt;/code&gt; action to use that method instead of always setting the value of &lt;code&gt;players&lt;/code&gt; to all players in the database.&lt;/p&gt;

&lt;p&gt;To do that, update &lt;code&gt;index&lt;/code&gt; in &lt;code&gt;app/controllers/players_controller.rb&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&gt;name&lt;/code&gt; search field to set its &lt;code&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&gt;players&lt;/code&gt; partial like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"Search by name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'player_filters'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"border border-blue-500 rounded p-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;autocomplete: &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"input-&amp;gt;search-form#search"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;value&lt;/code&gt; added to the &lt;code&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&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&gt;
  
  
  Session storage limitations
&lt;/h3&gt;

&lt;p&gt;We are relying on the &lt;code&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="https://guides.rubyonrails.org/v4.1.4/action_controller_overview.html#session" rel="noopener noreferrer"&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="https://github.com/rails/kredis" rel="noopener noreferrer"&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&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="https://viewcomponent.org/" rel="noopener noreferrer"&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&gt;
  
  
  Expand Filterable implementation
&lt;/h3&gt;

&lt;p&gt;Right now, &lt;code&gt;Filterable&lt;/code&gt; relies on each model to define &lt;code&gt;FILTER_PARAMS&lt;/code&gt; and a &lt;code&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&gt;filter&lt;/code&gt; in each model that we would benefit from moving &lt;code&gt;filter&lt;/code&gt; out of each model and into a &lt;code&gt;Filter&lt;/code&gt; class that defines some shared logic, like applying &lt;code&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&gt;Filterable&lt;/code&gt; pattern, take a look at the &lt;a href="https://www.stimulusreflexpatterns.com/patterns/filterable_reflex/" rel="noopener noreferrer"&gt;filterable_reflex&lt;/a&gt; from StimulusReflex Patterns.&lt;/p&gt;

&lt;h2&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="https://www.colby.so/posts/turbo-frames-on-rails" rel="noopener noreferrer"&gt;Turbo Frames&lt;/a&gt; and &lt;a href="https://www.colby.so/posts/turbo-streams-on-rails" rel="noopener noreferrer"&gt;Turbo Streams&lt;/a&gt; on Rails&lt;/li&gt;
&lt;li&gt;Dive into the &lt;a href="https://github.com/hotwired/turbo" rel="noopener noreferrer"&gt;Turbo&lt;/a&gt; and &lt;a href="https://github.com/hotwired/turbo-rails" rel="noopener noreferrer"&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="https://discuss.hotwired.dev/" rel="noopener noreferrer"&gt;hotwire discussion forums&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;(Shameless plug) Sign up for my monthly newsletter, &lt;a href="https://landing.mailerlite.com/webforms/landing/d7z0n0" rel="noopener noreferrer"&gt;Hotwiring Rails&lt;/a&gt;, to stay up to date on the latest developments in Rails-land&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;

</description>
      <category>rails</category>
      <category>tutorial</category>
      <category>ruby</category>
      <category>hotwire</category>
    </item>
    <item>
      <title>Turbo Frames on Rails</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Sat, 09 Oct 2021 13:47:04 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/turbo-frames-on-rails-5f4i</link>
      <guid>https://dev.to/davidcolbyatx/turbo-frames-on-rails-5f4i</guid>
      <description>&lt;p&gt;urbo, 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&gt;Turbo Drive&lt;/code&gt;, &lt;code&gt;Turbo Frames&lt;/code&gt;, &lt;code&gt;Turbo Streams&lt;/code&gt;, and &lt;code&gt;Turbo Native&lt;/code&gt;. Each is a valuable piece of the puzzle but today we’re going to focus on &lt;code&gt;Turbo Frames&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Turbo Frames “&lt;a href="https://turbo.hotwired.dev/handbook/frames"&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&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="https://github.com/hotwired/turbo-rails"&gt;turbo-rails&lt;/a&gt; gem) makes using frames in Rails a breeze.&lt;/p&gt;

&lt;h2&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&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;/p&gt;

&lt;h2&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&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&gt;
  
  
  What are Turbo Frames?
&lt;/h2&gt;

&lt;p&gt;At a high level, &lt;a href="https://turbo.hotwired.dev/handbook/frames"&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&gt;
  
  
  Constructing a frame
&lt;/h2&gt;

&lt;p&gt;A basic Turbo Frame, rendered with the &lt;a href="https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb"&gt;built-in helper&lt;/a&gt; from Turbo Rails (and using erb), looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"some_id"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    Some framed content
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only required argument for &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"some_id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    Some framed content
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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="https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb#L27"&gt;helper will use&lt;/a&gt; to generate a unique id via &lt;code&gt;dom_id(object.id)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="no"&gt;Comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frames can also receive an &lt;code&gt;src&lt;/code&gt; attribute. When &lt;code&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&gt;src&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"comments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;comments_path&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    Placeholder content
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the page will initially load the turbo frame with the placeholder div and then immediately a request to &lt;code&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&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&gt;comments&lt;/code&gt; example from above, when a request is made to &lt;code&gt;/comments&lt;/code&gt;, the server should respond with HTML that contains a frame like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"comments"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- A list of comments, perhaps --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="vi"&gt;@comments&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_comment_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ERB results in rendered HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"comment_1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/comments/1/edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Comment 1&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"comment_2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/comments/2/edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Comment 2&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"comment_3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/comments/3/edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Comment 3&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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&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&gt;turbo-rails&lt;/code&gt;, the presence of this header can be used to &lt;a href="https://github.com/hotwired/turbo-rails/blob/main/app/controllers/turbo/frames/frame_request.rb#L21"&gt;identify a Turbo Frame request&lt;/a&gt; and respond with appropriate content, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_request?&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"some_turbo_frame_partial"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"some_other_partial"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;turbo-rails&lt;/code&gt; responds to a Turbo Frame request, it &lt;a href="https://github.com/hotwired/turbo-rails/blob/main/app/controllers/turbo/frames/frame_request.rb#L16"&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="https://turbo.hotwired.dev/reference/drive"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline-field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"actions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After clicking to edit a comment then, the updated HTML after Turbo processes the response would look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Form content rendered from /edit --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"comments_1"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:3000/comments/1/edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/comments/1"&lt;/span&gt; &lt;span class="na"&gt;accept-charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"_method"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"patch"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"authenticity_token"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"some_token"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline-field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"comment_body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Body&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Comment 1"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"comment[body]"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"comment_body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"actions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"commit"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Update Comment"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- The other frames, untouched by the /edit request --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"comment_2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/comments/2/edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Comment 2&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"comment_3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/comments/3/edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Comment 3&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;comment#update&lt;/code&gt; controller action might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comment_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the show view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_comment_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&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&gt;data-turbo-frame="_top"&lt;/code&gt; to the element, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_comment_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;turbo_frame: &lt;/span&gt;&lt;span class="s2"&gt;"_top"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"user/1/profile"&lt;/span&gt; &lt;span class="na"&gt;data-turbo-frame=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Profile
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"user/1/favorites"&lt;/span&gt; &lt;span class="na"&gt;data-turbo-frame=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Favorites
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clicks on either of the &lt;code&gt;user&lt;/code&gt; links will generate a request with a &lt;code&gt;Turbo-Frame: main&lt;/code&gt; header and the response will update the content of the &lt;code&gt;main&lt;/code&gt; turbo frame, even though the links are not wrapped within the frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lazy loading Frames
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;src&lt;/code&gt; attribute on a frame can be combined with &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"lazy_frame"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;comments_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;loading: &lt;/span&gt;&lt;span class="s2"&gt;"lazy"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;I'm a loading spinner&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;loading="lazy"&lt;/code&gt; must be combined with a &lt;code&gt;src&lt;/code&gt; attribute, otherwise lazy loading does nothing&lt;/p&gt;

&lt;h2&gt;
  
  
  Frame events
&lt;/h2&gt;

&lt;p&gt;Turbo comes packed with &lt;a href="https://turbo.hotwired.dev/reference/events"&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="https://github.com/hotwired/turbo/releases/tag/v7.0.0-rc.2"&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="https://stimulus.hotwired.dev/"&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&gt;busy&lt;/code&gt; attribute. The &lt;a href="https://github.com/hotwired/turbo/blob/main/src/core/frames/frame_controller.ts#L172"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From Turbo's source code: turbo/src/core/frames/frame_controller.ts&lt;/span&gt;
&lt;span class="nx"&gt;requestStarted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FetchRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;busy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;requestFinished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FetchRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removeAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;busy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;turbo:frame-render&lt;/code&gt; and &lt;code&gt;turbo:frame-load&lt;/code&gt; events firing.&lt;/p&gt;

&lt;h2&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="https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb"&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="https://github.com/hotwired/turbo/issues/269"&gt;workarounds&lt;/a&gt; like appending a timestamp to the &lt;code&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&gt;src&lt;/code&gt; on each request.&lt;/p&gt;

&lt;p&gt;We can build a refreshable frame like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;%# app/views/games/show.html.erb %&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="vi"&gt;@game&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home_score&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;away_score&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Update score"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;game_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@game&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the first time a user clicks the update link, the &lt;code&gt;turbo-frame&lt;/code&gt; will be updated with an &lt;code&gt;src&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"game_10"&lt;/span&gt; &lt;span class="na"&gt;reloadable=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:3000/games/10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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="https://github.com/hotwired/turbo/pull/487"&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&gt;reloadable&lt;/code&gt; attribute from being exposed while keeping the behavior intact. In future versions of Turbo the &lt;code&gt;reloadable&lt;/code&gt; attribute may not longer exist or be neccessary.&lt;/p&gt;

&lt;h2&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&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="https://guides.rubyonrails.org/layouts_and_rendering.html#the-variants-option"&gt;variants&lt;/a&gt; to render different content based on the inbound request headers, to keep your controllers cleaner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:turbo_frame_request_variant&lt;/span&gt;
  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;turbo_frame_request_variant&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:turbo_frame&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_request?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach adds a before_action to the &lt;code&gt;ApplicationController&lt;/code&gt; that checks if the inbound request is a Turbo Frame request using the &lt;code&gt;turbo_frame_request?&lt;/code&gt; method &lt;a href="https://github.com/hotwired/turbo-rails/blob/main/app/controllers/turbo/frames/frame_request.rb#L21"&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&gt;new.html+turbo_frame.erb&lt;/code&gt;, for example) and render that variant when it exists. Otherwise, the default &lt;code&gt;html.erb&lt;/code&gt; view will be used&lt;/p&gt;

&lt;p&gt;This approach was originally described &lt;a href="https://github.com/hotwired/turbo/issues/378#issuecomment-914615544"&gt;here&lt;/a&gt;. While there was a &lt;a href="https://github.com/hotwired/turbo-rails/pull/250"&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&gt;turbo_frame_request?&lt;/code&gt; to detect Turbo Frame requests is the only support that will be added because &lt;a href="https://github.com/hotwired/turbo-rails/pull/250#issuecomment-942079485"&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&gt;
  
  
  Limitations &amp;amp; Gotchas
&lt;/h2&gt;

&lt;h3&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&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&gt;&amp;lt;%= turbo_frame_tag post, class: "classes" %&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="https://github.com/hotwired/turbo/issues/48#issuecomment-882575388"&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&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&gt;display: contents&lt;/code&gt;  to the &lt;code&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; element, as mentioned &lt;a href="https://github.com/hotwired/turbo/issues/48#issuecomment-925760725"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Response has no matching &amp;lt;turbo-frame id="target_id"&amp;gt; element
&lt;/code&gt;&lt;/pre&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&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="https://github.com/hotwired/turbo/releases/tag/v7.1.0"&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&gt;turbo-action&lt;/code&gt; data attribute to the form or link that triggers the frame navigation. Options for &lt;code&gt;turbo-action&lt;/code&gt; are &lt;code&gt;replace&lt;/code&gt;, to replace the current URL in the browser's history, or &lt;code&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&gt;advance&lt;/code&gt; to update the page URL with each new search. In Rails, using a standard &lt;code&gt;form_with&lt;/code&gt; search form, your code to push frame navigation into the browser's history might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;customers_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;turbo_frame: &lt;/span&gt;&lt;span class="s2"&gt;"customers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;turbo_action: &lt;/span&gt;&lt;span class="s2"&gt;"advance"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;%# Some form content goes here %&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&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="https://world.hey.com/dhh/rails-7-will-have-three-great-answers-to-javascript-in-2021-8d68191b"&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 mnay 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&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="https://www.colby.so/posts/handling-modal-forms-with-rails-and-hotwire"&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="https://thoughtbot.com/blog/hotwire-typeahead-searching"&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="https://www.colby.so/posts/conditional-rendering-with-turbo-stream-broadcasts"&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="https://www.colby.so/posts/turbo-streams-on-rails"&gt;exploration&lt;/a&gt; of Turbo Streams&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://landing.mailerlite.com/webforms/landing/d7z0n0"&gt;Sign up for&lt;/a&gt; my monthly newsletter to stay up to date on modern Ruby on Rails development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
      <category>hotwire</category>
    </item>
    <item>
      <title>Sort tables (almost) instantly with Ruby on Rails and Turbo Frames</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Mon, 20 Sep 2021 13:10:31 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/sort-tables-almost-instantly-with-ruby-on-rails-and-turbo-frames-ip7</link>
      <guid>https://dev.to/davidcolbyatx/sort-tables-almost-instantly-with-ruby-on-rails-and-turbo-frames-ip7</guid>
      <description>&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 developer friendly front ends.&lt;/p&gt;

&lt;p&gt;Yesterday, I &lt;a href="https://dev.to/davidcolbyatx/sort-tables-almost-instantly-with-ruby-on-rails-and-stimulusreflex-32d1"&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="https://turbo.hotwired.dev/handbook/frames" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgq9zn0ofue1oxigci0eo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgq9zn0ofue1oxigci0eo.gif" alt="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"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can demo the application for yourself &lt;a href="https://aqueous-stream-31641.herokuapp.com/" rel="noopener noreferrer"&gt;on Heroku&lt;/a&gt; (the free dyno may need a moment to wake up when you visit it) or view the &lt;a href="https://github.com/DavidColby/player_sorting_frames/tree/sortable" rel="noopener noreferrer"&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;/p&gt;

&lt;h2&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="https://github.com/rails/cssbundling-rails" rel="noopener noreferrer"&gt;css&lt;/a&gt; and &lt;a href="https://github.com/rails/jsbundling-rails" rel="noopener noreferrer"&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="https://github.com/DavidColby/player_sorting_frames/tree/main" rel="noopener noreferrer"&gt;from Github&lt;/a&gt;. The &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails new player_sort_frames --skip-javascript --skip-webpack-install --skip-turbolinks
cd player_sort_frames
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;/div&gt;



&lt;p&gt;Here, in addition to the bundling gems, we added &lt;a href="https://github.com/faker-ruby/faker" rel="noopener noreferrer"&gt;Faker&lt;/a&gt; to our project to allow us to quickly add seed data to the database and added a &lt;code&gt;Team&lt;/code&gt; model and &lt;code&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="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt; for CSS. From your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails javascript:install:webpack
rails css:install:tailwind
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails hotwire:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With everything installed, you can start up the server and build your assets with &lt;code&gt;bin/dev&lt;/code&gt; from your terminal.&lt;/p&gt;

&lt;h2&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&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&gt;app/views/players/index.html.erb&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-7xl mx-auto mt-12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"players"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;players: &lt;/span&gt;&lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The index is now rendering a partial (&lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch app/views/players/_players.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And fill the partial in like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"shadow overflow-hidden rounded border-b border-gray-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full bg-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;thead&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-800 text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-name"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Name&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-team"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Team&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-seasons"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Seasons&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;tbody&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-700"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seasons&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;player&lt;/code&gt; in the &lt;code&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&gt;db/seeds.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Dallas Mavericks'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'LA Clippers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'LA Lakers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'San Antonio Spurs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Boston Celtics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Miami Heat'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'New Orleans Pelicans'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;Faker&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;team: &lt;/span&gt;&lt;span class="no"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;seasons: &lt;/span&gt;&lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then from your terminal, seed the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails db:seed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you haven’t already, boot up the app and build assets with &lt;code&gt;bin/dev&lt;/code&gt; from your terminal, and then head to &lt;a href="http://localhost:3000/players" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffnl03han3wrgrot3glvl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffnl03han3wrgrot3glvl.png" alt="A screenshot of a data table with columns for name, team, and seasons"&gt;&lt;/a&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&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&gt;players&lt;/code&gt; partial to a &lt;code&gt;turbo_frame_tag&lt;/code&gt; with an id of &lt;code&gt;players&lt;/code&gt; and the same classes that the wrapper div had. In &lt;code&gt;app/views/players/_players.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"players"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"shadow overflow-hidden rounded border-b border-gray-200"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full bg-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- snip --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don’t forget to replace the closing &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt; with the &lt;code&gt;&amp;lt;% end %&amp;gt;&lt;/code&gt; tag!&lt;/p&gt;

&lt;p&gt;Now that the &lt;code&gt;players&lt;/code&gt; turbo frame wraps the content of the table, &lt;a href="https://turbo.hotwired.dev/handbook/frames" rel="noopener noreferrer"&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&gt;players&lt;/code&gt; partial still, update the table header like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;thead&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-800 text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-name"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-team"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="s2"&gt;"teams.name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Team"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-seasons"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="s2"&gt;"seasons"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Seasons"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’ve replaced the static text labeling each column with a call to the &lt;code&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&gt;app/helpers/players_helper.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;PlayersHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list_players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sort_link&lt;/code&gt; renders a standard Rails &lt;code&gt;link_to&lt;/code&gt;, using &lt;code&gt;label&lt;/code&gt; to display the link to the user, and &lt;code&gt;column&lt;/code&gt; to append a parameter to the generated URL.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;link_to&lt;/code&gt; points to &lt;code&gt;list_players_path&lt;/code&gt;, which we haven’t defined yet. Let’s do that next, in &lt;code&gt;config/routes.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:players&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;collection&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'list'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; 
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, we need to update our controller to define an action for the new &lt;code&gt;list&lt;/code&gt; route we’ve added.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;app/controllers/players_controller.rb&lt;/code&gt;, add a new &lt;code&gt;list&lt;/code&gt; method, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:team&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; asc"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'players'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;players: &lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&gt;params[:column]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;includes(:team)&lt;/code&gt; is necessary both to allow sorting players by their team name and for performance, since the &lt;code&gt;players&lt;/code&gt; partial makes a call to &lt;code&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Started GET "/players/list?column=seasons"
Processing by PlayersController#list as HTML
  Parameters: {"column"=&amp;gt;"seasons"}
  Player Load (0.5ms)  SELECT "players".* FROM "players" ORDER BY seasons asc
  ↳ app/views/players/_players.html.erb:18
  Team Load (0.2ms)  SELECT "teams".* FROM "teams" WHERE "teams"."id" IN (?, ?, ?, ?, ?, ?, ?)  [["id", 3], ["id", 1], ["id", 2], ["id", 6], ["id", 4], ["id", 5], ["id", 7]]
  ↳ app/views/players/_players.html.erb:18
  Rendered players/_players.html.erb (Duration: 5.9ms | Allocations: 3994)
Completed 200 OK in 7ms (Views: 5.6ms | ActiveRecord: 0.6ms | Allocations: 4224)
&lt;/code&gt;&lt;/pre&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&gt;players&lt;/code&gt;. Then we added a link to each of the header cells, pointing to &lt;code&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&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="https://turbo.hotwired.dev/handbook/frames#targeting-navigation-into-or-out-of-a-frame" rel="noopener noreferrer"&gt;break out of a frame&lt;/a&gt;, with &lt;code&gt;target=_top&lt;/code&gt; or use &lt;code&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&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6th2gyiv4brmv9wjpzm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6th2gyiv4brmv9wjpzm.gif" alt="A screen recording of a user clicking on column headers to sort a data table in ascending and descending order"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, we’ll modify the &lt;code&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&gt;list&lt;/code&gt; request.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;app/helpers/players_helper.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list_players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;direction: &lt;/span&gt;&lt;span class="n"&gt;next_direction&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list_players_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;direction: &lt;/span&gt;&lt;span class="s1"&gt;'asc'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;next_direction&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:direction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'asc'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'desc'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'asc'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sort_link&lt;/code&gt; now adds a second parameter into the &lt;code&gt;link_to&lt;/code&gt;. The new &lt;code&gt;direction&lt;/code&gt; parameter is set to the value of the &lt;code&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&gt;direction&lt;/code&gt; is set to ascending.&lt;/p&gt;

&lt;p&gt;&lt;code&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&gt;list&lt;/code&gt; method in &lt;code&gt;players_controller.rb&lt;/code&gt;  to use the new &lt;code&gt;direction&lt;/code&gt; parameter in the &lt;code&gt;order&lt;/code&gt; clause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:team&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:direction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# snip&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;thead&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-800 text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-name"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_indicator&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-team"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_indicator&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"teams.name"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="s2"&gt;"teams.name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Team"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-seasons"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_indicator&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"seasons"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;sort_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="s2"&gt;"seasons"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Seasons"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’ve added a conditional call to &lt;code&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&gt;if params[:column]&lt;/code&gt; to skip the &lt;code&gt;sort_indicator&lt;/code&gt; call on inactive columns.&lt;/p&gt;

&lt;p&gt;We also added &lt;code&gt;relative&lt;/code&gt; to the &lt;code&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&gt;sort_indicator&lt;/code&gt; in &lt;code&gt;app/helpers/players_helper.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort_indicator&lt;/span&gt;
  &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"sort sort-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:direction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sort_indicator&lt;/code&gt; simply returns a span with a class that matches the current sort direction, read from &lt;code&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&gt;app/assets/stylesheets/application.css&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.sort&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.sort-desc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.sort-asc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the CSS added and our new &lt;code&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxnp8q3t87ligf2qd7nw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxnp8q3t87ligf2qd7nw.gif" alt="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"&gt;&lt;/a&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&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="https://hotwired.dev/" rel="noopener noreferrer"&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="https://docs.stimulusreflex.com/" rel="noopener noreferrer"&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="https://turbo.hotwired.dev/handbook/introduction" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Dive in to the Turbo Rails &lt;a href="https://github.com/hotwired/turbo-rails" rel="noopener noreferrer"&gt;source code&lt;/a&gt; on Github&lt;/li&gt;
&lt;li&gt;Spend time with &lt;a href="https://stimulus.hotwired.dev/handbook/introduction" rel="noopener noreferrer"&gt;Stimulus&lt;/a&gt;, the other side of the Hotwire stack&lt;/li&gt;
&lt;li&gt;Learn from the &lt;a href="https://discuss.hotwired.dev/" rel="noopener noreferrer"&gt;community&lt;/a&gt; in the Hotwire discussion forum&lt;/li&gt;
&lt;li&gt;Join the &lt;a href="https://discord.gg/stimulus-reflex" rel="noopener noreferrer"&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;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>hotwire</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Sort tables (almost) instantly with Ruby on Rails and StimulusReflex</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Sun, 19 Sep 2021 14:03:38 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/sort-tables-almost-instantly-with-ruby-on-rails-and-stimulusreflex-32d1</link>
      <guid>https://dev.to/davidcolbyatx/sort-tables-almost-instantly-with-ruby-on-rails-and-stimulusreflex-32d1</guid>
      <description>&lt;p&gt;Today we’re going to use Ruby on Rails and &lt;a href="https://docs.stimulusreflex.com/" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwitj4pcuj71mmslm5y8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwitj4pcuj71mmslm5y8.gif" alt="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"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can view the finished product &lt;a href="https://stark-peak-59471.herokuapp.com/" rel="noopener noreferrer"&gt;on Heroku&lt;/a&gt;, or find the full source on &lt;a href="https://github.com/DavidColby/sortable_players/tree/sortable" rel="noopener noreferrer"&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, 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;/p&gt;

&lt;h2&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="https://github.com/DavidColby/sortable_players/tree/main" rel="noopener noreferrer"&gt;repo&lt;/a&gt; and skip ahead to the Building the Table section. The &lt;code&gt;main&lt;/code&gt; branch of the example 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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails new player_sorting -T
cd 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;/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;p&gt;yarn add tailwindcss@latest postcss@latest autoprefixer@latest&lt;br&gt;
npx tailwindcss init&lt;br&gt;
mkdir app/javascript/stylesheets&lt;br&gt;
touch app/javascript/stylesheets/application.scss&lt;/p&gt;

&lt;p&gt;And then update &lt;code&gt;tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;purge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/**/*/*.html.erb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/helpers/**/*/*.rb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/**/*/*.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/**/*/*.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/**/*/*.react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;darkMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And postcss.config.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tailwindcss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./tailwind.config.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postcss-import&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postcss-flexbugs-fixes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postcss-preset-env&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
      &lt;span class="na"&gt;autoprefixer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;flexbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-2009&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we’ll update &lt;code&gt;app/javascripts/stylesheets/application.scss&lt;/code&gt; to import Tailwind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"tailwindcss/base"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"tailwindcss/components"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"tailwindcss/utilities"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then include that stylesheet in &lt;code&gt;app/javascripts/packs/application.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../stylesheets/application.scss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, update the application layout to include the stylesheet generated by webpacker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload', media: 'all' %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whew. We’re through the setup and ready to start building.&lt;/p&gt;

&lt;h2&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&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&gt;Team&lt;/code&gt; model created during setup in the context of &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch app/views/players/_players.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"shadow overflow-hidden rounded border-b border-gray-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full bg-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;thead&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-800 text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-name"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Name&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-team"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Team&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-seasons"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Seasons&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;tbody&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-700"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seasons&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;ids&lt;/code&gt; set on the wrapper div (&lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-7xl mx-auto mt-12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"players"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;players: &lt;/span&gt;&lt;span class="vi"&gt;@players&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&gt;db/seeds.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Dallas Mavericks'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'LA Clippers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'LA Lakers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'San Antonio Spurs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Boston Celtics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Miami Heat'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'New Orleans Pelicans'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;Faker&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;team: &lt;/span&gt;&lt;span class="no"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;seasons: &lt;/span&gt;&lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, from your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails db:seed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now start up your Rails server and head to &lt;a href="http://localhost:3000/players" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2f73dyit8vhlz18bloha.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2f73dyit8vhlz18bloha.png" alt="A screenshot of a data table with columns for name, team, and seasons"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next up, we’ll build our sorting mechanism with StimulusReflex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sorting with StimulusReflex
&lt;/h2&gt;

&lt;p&gt;To sort the table, we’re going to create a &lt;a href="https://docs.stimulusreflex.com/rtfm/reflex-classes" rel="noopener noreferrer"&gt;reflex class&lt;/a&gt; with a single action, &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g stimulus_reflex Table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generator creates both a &lt;code&gt;reflex&lt;/code&gt; in &lt;code&gt;app/reflexes&lt;/code&gt; and a related Stimulus controller in &lt;code&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. Instead, we’ll focus on the reflex found at &lt;code&gt;app/reflexes/table_reflex.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Fill that reflex in with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TableReflex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationReflex&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort&lt;/span&gt;
    &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;column&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;direction&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;morph&lt;/span&gt; &lt;span class="s1"&gt;'#players'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'players'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;players: &lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line of &lt;code&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="https://docs.stimulusreflex.com/rtfm/reflex-classes#building-your-reflex-action" rel="noopener noreferrer"&gt;variety of properties&lt;/a&gt;. In our case, the property we’re interested in is &lt;a href="https://docs.stimulusreflex.com/rtfm/reflex-classes#element" rel="noopener noreferrer"&gt;element&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&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&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&gt;column&lt;/code&gt; and &lt;code&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="https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs" rel="noopener noreferrer"&gt;selector morph&lt;/a&gt; to update the DOM, replacing the content of the &lt;code&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&gt;players&lt;/code&gt; partial, update the header row like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; 
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-name"&lt;/span&gt; 
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;
    &lt;span class="na"&gt;data-reflex=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;Table#sort"&lt;/span&gt;
    &lt;span class="na"&gt;data-column=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
    &lt;span class="na"&gt;data-direction=&lt;/span&gt;&lt;span class="s"&gt;"asc"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Name&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; 
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-team"&lt;/span&gt; 
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;
    &lt;span class="na"&gt;data-reflex=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;Table#sort"&lt;/span&gt;
    &lt;span class="na"&gt;data-column=&lt;/span&gt;&lt;span class="s"&gt;"teams.name"&lt;/span&gt;
    &lt;span class="na"&gt;data-direction=&lt;/span&gt;&lt;span class="s"&gt;"asc"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Team&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; 
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"players-seasons"&lt;/span&gt; 
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"&lt;/span&gt;
    &lt;span class="na"&gt;data-reflex=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;Table#sort"&lt;/span&gt;
    &lt;span class="na"&gt;data-column=&lt;/span&gt;&lt;span class="s"&gt;"seasons"&lt;/span&gt;
    &lt;span class="na"&gt;data-direction=&lt;/span&gt;&lt;span class="s"&gt;"asc"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Seasons&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;data-reflex&lt;/code&gt; is used to &lt;a href="https://docs.stimulusreflex.com/rtfm/reflexes#declaring-a-reflex-in-html-with-data-attributes" rel="noopener noreferrer"&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&gt;click&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each cell also gets a unique &lt;code&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&gt;direction&lt;/code&gt; of &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;column&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;direction&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;morph&lt;/span&gt; &lt;span class="s1"&gt;'#players'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'players'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;players: &lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sort&lt;/code&gt; method we’ve defined matches the name of the &lt;code&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&gt;element&lt;/code&gt;, giving us access to the element’s data attributes, which we access through &lt;code&gt;element.dataset&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once the value of &lt;code&gt;players&lt;/code&gt; is set by the database query, we use a &lt;code&gt;selector&lt;/code&gt; &lt;a href="https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs" rel="noopener noreferrer"&gt;morph&lt;/a&gt; to tell the browser to update the element with the id of &lt;code&gt;players&lt;/code&gt; with the content of the &lt;code&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&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&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&gt;Players&lt;/code&gt; table. ActiveRecord knows how to order by &lt;code&gt;name&lt;/code&gt; and &lt;code&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&gt;teams.name&lt;/code&gt; to the &lt;code&gt;order&lt;/code&gt; call in our query, which ActiveRecord trips over with an error like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# The text will vary depending on the database adapter you're using!
Reflex Table#sort failed: SQLite3::SQLException: no such column: teams.name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can fix this by updating the query slightly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:team&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;column&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;direction&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we added &lt;code&gt;includes(:team)&lt;/code&gt; to the existing order query, making the &lt;code&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&gt;joins&lt;/code&gt; instead of &lt;code&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&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="https://github.com/rails/rails/pull/27947" rel="noopener noreferrer"&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&gt;order&lt;/code&gt;.&lt;/p&gt;

&lt;h2&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwp9fp6x6v1b80m8os8r8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwp9fp6x6v1b80m8os8r8.gif" alt="A screen recording of a user clicking on column headers to sort a data table in ascending and descending order"&gt;&lt;/a&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&gt;sort&lt;/code&gt; reflex is called, the right direction can be sent to the &lt;code&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="https://docs.stimulusreflex.com/rtfm/cableready" rel="noopener noreferrer"&gt;CableReady’s&lt;/a&gt; tight integration with StimulusReflex. We’ll update the &lt;code&gt;sort&lt;/code&gt; reflex to include a &lt;code&gt;cable_ready&lt;/code&gt; &lt;a href="https://cableready.stimulusreflex.com/reference/operations" rel="noopener noreferrer"&gt;operation&lt;/a&gt; that changes the &lt;code&gt;direction&lt;/code&gt; data attribute of the &lt;code&gt;element&lt;/code&gt; that triggered the reflex.&lt;/p&gt;

&lt;p&gt;To do this, update &lt;code&gt;TableReflex&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort&lt;/span&gt;
  &lt;span class="c1"&gt;# snip&lt;/span&gt;
  &lt;span class="n"&gt;set_sort_direction&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;next_direction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'asc'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'desc'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'asc'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_sort_direction&lt;/span&gt;
  &lt;span class="n"&gt;cable_ready&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_dataset_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;selector: &lt;/span&gt;&lt;span class="s2"&gt;"#&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'direction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;next_direction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’ve added two private methods to the &lt;code&gt;TableReflex&lt;/code&gt; class. &lt;code&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&gt;set_sort_direction&lt;/code&gt; is more interesting. In it, we use CableReady’s &lt;code&gt;set_dataset_property&lt;/code&gt; &lt;a href="https://cableready.stimulusreflex.com/v/v5/reference/operations/element-mutations#set_dataset_property" rel="noopener noreferrer"&gt;operation&lt;/a&gt; to set the value of the element’s &lt;code&gt;direction&lt;/code&gt; data attribute to the value of  &lt;code&gt;next_direction&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;Finally, we call &lt;code&gt;set_sort_direction&lt;/code&gt; in the &lt;code&gt;sort&lt;/code&gt; reflex, which adds the &lt;code&gt;set_dataset_property&lt;/code&gt; operation to the queue each time the &lt;code&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&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="https://docs.stimulusreflex.com/rtfm/cableready#order-of-operations" rel="noopener noreferrer"&gt;specific order of operations&lt;/a&gt; is always followed.&lt;/p&gt;

&lt;p&gt;First, &lt;a href="https://cableready.stimulusreflex.com/reference/methods" rel="noopener noreferrer"&gt;broadcasted&lt;/a&gt; CableReady &lt;code&gt;operations&lt;/code&gt; execute. Next, StimulusReflex &lt;code&gt;morphs&lt;/code&gt; execute. Finally, CableReady &lt;code&gt;operations&lt;/code&gt; that are not &lt;code&gt;broadcasted&lt;/code&gt; execute (that’s our &lt;code&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&gt;direction&lt;/code&gt; data attribute reset to &lt;code&gt;asc&lt;/code&gt; when &lt;code&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&gt;set_dataset_property&lt;/code&gt; runs and updates the value of &lt;code&gt;direction&lt;/code&gt; on the currently active sort column.&lt;/p&gt;

&lt;p&gt;If we appended &lt;code&gt;.broadcast&lt;/code&gt; to the &lt;code&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&gt;morph&lt;/code&gt;, causing the CableReady update to be overwritten by the &lt;code&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&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&gt;data-direction&lt;/code&gt; value set to &lt;code&gt;asc&lt;/code&gt; before the active sort column is updated by the CableReady &lt;code&gt;set_dataset_property&lt;/code&gt; operation.&lt;/p&gt;

&lt;p&gt;Since the value of &lt;code&gt;data-direction&lt;/code&gt; is already &lt;code&gt;asc&lt;/code&gt;, if the next sort direction is &lt;code&gt;asc&lt;/code&gt;, &lt;code&gt;set_dataset_property&lt;/code&gt; won’t do anything useful. &lt;/p&gt;

&lt;p&gt;Let’s update &lt;code&gt;sort&lt;/code&gt; to skip the CableReady operation in that case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort&lt;/span&gt;
&lt;span class="c1"&gt;# snip&lt;/span&gt;
  &lt;span class="n"&gt;set_sort_direction&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;next_direction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'desc'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&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&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn0f3lgl6moc2nb8pdq2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn0f3lgl6moc2nb8pdq2q.png" alt="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"&gt;&lt;/a&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&gt;application.scss&lt;/code&gt; file to keep things simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.sort&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.sort-desc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.sort-asc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll use the base &lt;code&gt;.sort&lt;/code&gt; class along with a dynamic &lt;code&gt;.sort-asc&lt;/code&gt; or &lt;code&gt;.sort-desc&lt;/code&gt; to display the sort indicator. If you’re interested in how this CSS works, &lt;a href="https://www.freecodecamp.org/news/css-shapes-explained-how-to-draw-a-circle-triangle-and-more-using-pure-css/" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch app/views/players/_sort_indicator.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And fill that in with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sort sort-&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing fancy here, just appending the local &lt;code&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&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&gt;morph&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TableReflex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationReflex&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort&lt;/span&gt;
    &lt;span class="c1"&gt;# snip&lt;/span&gt;
    &lt;span class="n"&gt;insert_indicator&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

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

  &lt;span class="c1"&gt;# snip&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_indicator&lt;/span&gt;
    &lt;span class="n"&gt;cable_ready&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="ss"&gt;selector: &lt;/span&gt;&lt;span class="s2"&gt;"#&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'players/sort_indicator'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;direction: &lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we added another private method to the reflex to handle inserting the sort indicator when the &lt;code&gt;sort&lt;/code&gt; reflex is called.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;insert_indicator&lt;/code&gt; uses CableReady’s &lt;code&gt;prepend&lt;/code&gt; &lt;a href="https://cableready.stimulusreflex.com/reference/operations/dom-mutations#prepend" rel="noopener noreferrer"&gt;operation&lt;/a&gt; to insert the content of the &lt;code&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7cks9k338rlj13z03u7o.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7cks9k338rlj13z03u7o.gif" alt="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"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  An implementation note
&lt;/h2&gt;

&lt;p&gt;During this article I made a choice to use &lt;code&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&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&gt;sort&lt;/code&gt; reflex. We could then read those variables when rendering the &lt;code&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&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&gt;cable_ready&lt;/code&gt; operations to simplify the view.&lt;/p&gt;

&lt;p&gt;Using &lt;code&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&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="https://beastmode.leastbad.com/" rel="noopener noreferrer"&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="https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs" rel="noopener noreferrer"&gt;bypassing ActionDispatch’s overhead&lt;/a&gt; with &lt;code&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="https://www.stimulusreflexpatterns.com/patterns/" rel="noopener noreferrer"&gt;StimulusReflex patterns&lt;/a&gt; for thoughtfully designed solutions in StimulusReflex, including &lt;a href="https://www.stimulusreflexpatterns.com/patterns/filterable_reflex/" rel="noopener noreferrer"&gt;filterable&lt;/a&gt; for working with complex sorting and filtering requirements&lt;/li&gt;
&lt;li&gt;Join the &lt;a href="https://discord.com/invite/stimulus-reflex" rel="noopener noreferrer"&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;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Server-rendered modal forms with Ruby on Rails, CableReady, Mrujs, Stimulus, and Tailwind</title>
      <dc:creator>David Colby</dc:creator>
      <pubDate>Fri, 10 Sep 2021 03:09:11 +0000</pubDate>
      <link>https://dev.to/davidcolbyatx/server-rendered-modal-forms-on-rails-with-cableready-mrujs-stimulus-and-tailwind-2mne</link>
      <guid>https://dev.to/davidcolbyatx/server-rendered-modal-forms-on-rails-with-cableready-mrujs-stimulus-and-tailwind-2mne</guid>
      <description>&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 and 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="https://stimulus.hotwired.dev/" rel="noopener noreferrer"&gt;Stimulus&lt;/a&gt; for the front-end interactivity, &lt;a href="https://cableready.stimulusreflex.com/v/v5/" rel="noopener noreferrer"&gt;CableReady’s&lt;/a&gt; brand new &lt;a href="https://cableready.stimulusreflex.com/v/v5/cable-car" rel="noopener noreferrer"&gt;CableCar feature&lt;/a&gt; to send content back from the server, and &lt;a href="https://mrujs.com/" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn7k6dxxmvs81mb4ezy91.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn7k6dxxmvs81mb4ezy91.gif" alt="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'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's change immediately."&gt;&lt;/a&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="https://github.com/DavidColby/tiny_crm/tree/implement-modals" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's dive in!&lt;/p&gt;

&lt;h2&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="https://github.com/DavidColby/tiny_crm/tree/main" rel="noopener noreferrer"&gt;this repo&lt;/a&gt; and then scroll down to the Customers Layout 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="https://github.com/rails/jsbundling-rails" rel="noopener noreferrer"&gt;jsbundling-rails&lt;/a&gt; and &lt;a href="https://github.com/rails/cssbundling-rails" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails new tiny_crm --skip-webpack-install --skip-javascript
cd 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;/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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle add hotwire-rails
be rails hotwire:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then update your Gemfile to pull in the latest cable_ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"cable_ready"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;github: &lt;/span&gt;&lt;span class="s2"&gt;"stimulusreflex/cable_ready"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle
yarn add mrujs cable_ready @rails/actioncable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, update &lt;code&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="https://mrujs.com/how-tos/integrate-cablecar" rel="noopener noreferrer"&gt;CableCar plugin&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./controllers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mrujs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrujs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CableReady&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cable_ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CableCar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrujs/plugins&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Turbo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/turbo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Turbo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Turbo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;mrujs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CableCar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CableReady&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g scaffold Customer name:string
rails db:migrate
&lt;/code&gt;&lt;/pre&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&gt;
  
  
  Customers layout
&lt;/h2&gt;

&lt;p&gt;First, we’ll apply some basic styling to the customers index page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto mt-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"modal"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-baseline mb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-3xl text-gray-900"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Customers&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New Customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_customer_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-600"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"click-&amp;gt;modal#open"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s1"&gt;'modal'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"customers"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col items-baseline space-y-6 p-4 shadow-lg rounded"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="vi"&gt;@customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 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&gt;controller="modal"&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&gt;modal&lt;/code&gt; controller in its &lt;code&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&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&gt;modal&lt;/code&gt; and &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch app/views/customers/_customer.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The customer partial just renders the customer’s name for now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-700 border-b border-gray-200 w-full pb-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next create the modal partial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch app/views/customers/_modal.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-modal-target=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;
     &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden fixed inset-0 overflow-y-auto flex items-center justify-center"&lt;/span&gt;
     &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"z-index: 9999;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-lg max-h-screen w-full relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"m-1 bg-white rounded shadow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"px-4 py-5 border-b border-gray-200 sm:px-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-lg leading-6 font-medium text-gray-900"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          Customer
        &lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"customer_form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important items here are the &lt;code&gt;modal-target="container"&lt;/code&gt;  data attribute, which the Stimulus controller will use to open/close the modal and the empty &lt;code&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&gt;bin/dev&lt;/code&gt;, head to &lt;a href="http://localhost:3000/customers" rel="noopener noreferrer"&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&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&gt;
  
  
  Showing the new customer modal
&lt;/h2&gt;

&lt;p&gt;First we need to create a new Stimulus controller, using the handy generator. From your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g stimulus modal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And fill the controller in with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Credit: This controller is an edited-to-the-essentials version&lt;/span&gt;
&lt;span class="c1"&gt;// of the modal component created by @excid3 as part of the essential &lt;/span&gt;
&lt;span class="c1"&gt;// tailwind-stimulus-components package found here:&lt;/span&gt;
&lt;span class="c1"&gt;// https://github.com/excid3/tailwindcss-stimulus-components&lt;/span&gt;

&lt;span class="c1"&gt;// In production, use the full component from the &lt;/span&gt;
&lt;span class="c1"&gt;// library or expand this controller to allow for &lt;/span&gt;
&lt;span class="c1"&gt;// keyboard closing and dealing with scroll positions&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggleClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backgroundId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modal-background&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backgroundHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_backgroundHTML&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowBackgroundClose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inset-x-0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;overflow-hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;containerTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggleClass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertAdjacentHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backgroundHtml&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backgroundId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;containerTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggleClass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;_backgroundHTML&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;div id="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backgroundId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" class="fixed top-0 left-0 w-full h-full" style="background-color: rgba(0, 0, 0, 0.7); z-index: 9998;"&amp;gt;&amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s a lot of JavaScript here, but it isn’t doing anything too fancy.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;connect&lt;/code&gt;, we set default values the controller needs to function. &lt;/p&gt;

&lt;p&gt;&lt;code&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&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="https://github.com/excid3/tailwindcss-stimulus-components" rel="noopener noreferrer"&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&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="https://mrujs.com/how-tos/integrate-cablecar" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt;, we’ll do that by updating the link on the index page like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New Customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_customer_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"text-blue-600"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"click-&amp;gt;modal#open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;cable_car: &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’ve added &lt;code&gt;data-cable-car=""&lt;/code&gt;, and Mrujs &lt;a href="https://mrujs.com/how-tos/integrate-cablecar#using-cablecar" rel="noopener noreferrer"&gt;takes care of the rest&lt;/a&gt; for us.&lt;/p&gt;

&lt;p&gt;With this change in place, when the user clicks on the New Customer link, an AJAX request will be sent to &lt;code&gt;customers#new&lt;/code&gt;.&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"customer_form"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"px-4 pt-5 pb-4 sm:p-6 sm:pb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"p-4 border border-red-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;
          Could not save customer
        &lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-group"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rounded-b mt-6 px-4 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"w-full sm:col-start-2 bg-blue-600 px-4 py-2 mb-4 text-white rounded-sm hover:bg-blue-700"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;modal#close"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"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"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Cancel
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&gt;data-action&lt;/code&gt; assigned to the close button, which fires the &lt;code&gt;close&lt;/code&gt; function we defined in the &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add @tailwindcss/forms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then update &lt;code&gt;tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;purge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/views/**/*.html.erb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/helpers/**/*.rb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/javascript/**/*.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tailwindcss/forms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we need to update the &lt;code&gt;new&lt;/code&gt; method in the &lt;code&gt;CustomersController&lt;/code&gt; to render &lt;a href="https://cableready.stimulusreflex.com/v/v5/reference/operations" rel="noopener noreferrer"&gt;CableReady operations&lt;/a&gt;, using the newly introduced &lt;a href="https://cableready.stimulusreflex.com/v/v5/cable-car#ajax-mode" rel="noopener noreferrer"&gt;CableCar&lt;/a&gt;. To do that, we’ll make two changes to the &lt;code&gt;CustomersController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First update the controller to &lt;a href="https://cableready.stimulusreflex.com/v/v5/cableready-everywhere#controller-actions" rel="noopener noreferrer"&gt;include CableReady::Broadcaster&lt;/a&gt; to give the controller access to &lt;code&gt;cable_car&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CableReady&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Broadcaster&lt;/span&gt;
  &lt;span class="c1"&gt;# snip&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feel free to place the &lt;code&gt;include&lt;/code&gt; in &lt;code&gt;ApplicationController&lt;/code&gt; if you prefer.&lt;/p&gt;

&lt;p&gt;Then update the &lt;code&gt;CustomersController&lt;/code&gt; new method as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outer_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#customer_form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’re rendering the form partial to a string which we then pass to cable_car and use in an &lt;a href="https://cableready.stimulusreflex.com/v/v5/reference/operations/dom-mutations#outer_html" rel="noopener noreferrer"&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="http://localhost:3000/customers" rel="noopener noreferrer"&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&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 cable car data attribute, just like we added to the new customer link in the last section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"customer_form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;cable_car: &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&gt;customers_controller.rb&lt;/code&gt; and update the create method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#customers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: Handle errors&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’re again rendering a partial to a string and passing that string to an operation (this time, &lt;code&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="https://cableready.stimulusreflex.com/reference/operations/dom-mutations#prepend" rel="noopener noreferrer"&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mqjv6bi2d1n77iu8bt1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mqjv6bi2d1n77iu8bt1.gif" alt="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"&gt;&lt;/a&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="https://cableready.stimulusreflex.com/v/v5/cableready-101#method-chaining" rel="noopener noreferrer"&gt;chaining operations&lt;/a&gt; and &lt;a href="https://cableready.stimulusreflex.com/v/v5/reference/operations/event-dispatch" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# snip&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#customers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'submit:success'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dispatch_event&lt;/code&gt; &lt;a href="https://cableready.stimulusreflex.com/reference/operations/event-dispatch" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit:success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// snip&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the 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&gt;append&lt;/code&gt; and the &lt;code&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&gt;submit:success&lt;/code&gt; event listener closes the modal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling errors
&lt;/h2&gt;

&lt;p&gt;Wonderful work stuff so far. Next we'll deal with form errors using &lt;code&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&gt;validates_presence_of :name&lt;/code&gt; to &lt;code&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#customers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'submit:success'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#customer_form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&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&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element itself, we can just use one &lt;code&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9k82d9qhkggex2mrw93e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9k82d9qhkggex2mrw93e.gif" alt="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"&gt;&lt;/a&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&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&gt;cable_car&lt;/code&gt; enabled modal link to the &lt;code&gt;customer&lt;/code&gt; partial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-700 border-b border-gray-200 w-full pb-2"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="s2"&gt;"customer-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_customer_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;cable_car: &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"click-&amp;gt;modal#open"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;CustomersController&lt;/code&gt; to adjust the &lt;code&gt;edit&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt; methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;
  &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#customer_form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"#customer-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'submit:success'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#customer_form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should look pretty familiar. The &lt;code&gt;edit&lt;/code&gt; method is a mirror of the &lt;code&gt;new&lt;/code&gt; method, and the &lt;code&gt;update&lt;/code&gt; method is a mirror of the &lt;code&gt;create&lt;/code&gt; method. Again, we dispatch the &lt;code&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&gt;data-controller="modal"&lt;/code&gt; declaration one level up the DOM tree. In &lt;code&gt;customers/index.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto mt-8"&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"modal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-baseline mb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Snip --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4d7u1ml7pl6smiy2id8s.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4d7u1ml7pl6smiy2id8s.gif" alt="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"&gt;&lt;/a&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&gt;modal&lt;/code&gt; controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;modal:loaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;containerTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggleClass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit:success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inset-x-0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;overflow-hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertAdjacentHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backgroundHtml&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backgroundId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we updated &lt;code&gt;open&lt;/code&gt; to move the &lt;code&gt;containerTarget.classList.remove&lt;/code&gt; call from happening instantly to happening in response to &lt;code&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&gt;modal:loaded&lt;/code&gt; event never occurs and so &lt;code&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&gt;CustomersController&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outer_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#customer_form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'modal:loaded'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;
  &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;operations: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outer_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#customer_form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'modal:loaded'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In both the &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;edit&lt;/code&gt; methods, we again take advantage of CableReady’s chainable operations to dispatch &lt;code&gt;modal:loaded&lt;/code&gt; after the &lt;code&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy9ivkgihj69ants97aqb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy9ivkgihj69ants97aqb.gif" alt="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'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's change immediately."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The single &lt;code&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&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="https://docs.stimulusreflex.com/" rel="noopener noreferrer"&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&gt;rails/ujs&lt;/code&gt;, which is no longer under active development and which will be &lt;a href="https://github.com/rails/rails/pull/43112" rel="noopener noreferrer"&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="https://mrujs.com/tutorials/practical-guide-to-mrujs" rel="noopener noreferrer"&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="https://www.colby.so/posts/handling-modal-forms-with-rails-and-hotwire" rel="noopener noreferrer"&gt;here's a guide&lt;/a&gt; for that).&lt;/p&gt;

&lt;p&gt;While the full &lt;a href="https://hotwired.dev/" rel="noopener noreferrer"&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="https://cableready.stimulusreflex.com/" rel="noopener noreferrer"&gt;CableReady documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://stimulus.hotwired.dev/reference/controllers" rel="noopener noreferrer"&gt;Stimulus docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://mrujs.com/tutorials/getting-started" rel="noopener noreferrer"&gt;Mrujs docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Explore the &lt;a href="https://docs.stimulusreflex.com/" rel="noopener noreferrer"&gt;StimulusReflex documentation&lt;/a&gt; when you’re ready&lt;/li&gt;
&lt;li&gt;Join the &lt;a href="https://discord.gg/stimulus-reflex" rel="noopener noreferrer"&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="https://landing.mailerlite.com/webforms/landing/d7z0n0" rel="noopener noreferrer"&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;

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