Andy from Webcrunch

Subscribe for email updates:

Let's build with Hotwire and Rails - Data filtering
Portrait of Andy Leverenz
Andy Leverenz

April 7, 2023

Last updated November 13, 2023

Let's build with Hotwire and Rails - Data filtering

Welcome to another edition of my Hotwire and Rails series, where I take you through a journey of creating old conventions that used to require JavaScript and bringing them to life using Hotwire and Rails.

This guide is a simple one aimed at showing foundational principles for using Hotwire. We'll build a simple database of quotes from the movie Dumb and Dumber and present simple filters to cycle through that data.

The filters toggle the order of different types of data related to a Quote model and then, upon click, will instantly sort themselves in ascending or descending fashion.

It's important to note that this functionality is completely possible with legacy Rails applications. Still, the main difference lies in the request cycle when a user might click a sortable link.

Additionally, we can now embed forms that render in place to edit specific quotes on the fly.

Hotwire brings us a lot of power which is very exciting to see as you begin to construct applications that used to take loads of JavaScript, JSON, and third-party libraries to get the job done.

Create an app

Most of my new apps begin like the following. We won't use JavaScript in this guide, but we will leverage a bit of Tailwind CSS.

rails new turbo_filter_quotes -c tailwind -j esbuild

Add a development and testing dependency

bundle add faker

I like using the Faker gem to easily generate dummy data for a given app. This is useful for testing, design, development, and more.

Feel free to use any other data. I leveraged the library of Dumb and Dumber quotes because that movie is a favorite of mine.

Generate the Quote model

I'll generate a scaffold for the Quote model to save time. It will feature three string-type columns.

rails g scaffold Quote quote:string character:string action:string

Next, you will want to migrate your database.

rails db:migrate

Fill in some demo data

With the data layer intact, we can now add some dummy data using the function of the built-in seed with rails.

# db/seeds.rb

20.times do
  Quote.create(
    quote: Faker::TvShows::DumbAndDumber.quote,
    character: Faker::TvShows::DumbAndDumber.character,
    actor: Faker::TvShows::DumbAndDumber.actor
  )
end

We can create twenty quotes using ruby code with demo data from the faker gem. Run the following command to do just that.

rails db:seed

Boot the app and name a root route

We have yet to boot the Rails application, but I know the root route is not configured. Let's make it the quotes#index action that was previously generated by the scaffold command we ran before.

# config/routes.rb

root to: "quotes#index"

Build the controller query

To sort data, there needs to be an order method appended to a given model. This extracts all the SQL logic thanks to ActiveRecord within Rails. The neat thing about ruby is dynamically passes options to these queries based on interactions on the front end. We'll get to this code's UI/UX portions in a bit, but the controller code can be a one-liner for now.

# app/controllers/quotes_controller.rb

def index
  @quotes = Quote.order("#{params[:column]} #{params[:direction]}")
end

Enter some turbo magic

In the views comes the real magic that allows us to filter data in real-time. Here's where I ended up with the index view.

<!-- app/views/quotes/index.html.erb -->

<div class="max-w-3xl mx-auto px-4 my-16">
  <div class="flex items-center justify-between pb-6 border-b dark:border-slate-700">
    <h1 class="font-bold text-3xl">Dumb and Dumber Quotes</h1>
    <div class="flex items-center justify-end">
      <%= link_to "New Quote", new_quote_path, class: "px-3 py-2 rounded bg-teal-500 hover:bg-teal-600 text-white" %>
    </div>
  </div>

  <%= turbo_frame_tag "quotes_data" do %>
    <div class="flex items-center p-3 bg-teal-600 text-white space-x-6 rounded">
      <%= link_to "Character", quotes_path(column: "character", direction: direction), class: "underline" %>
      <%= filter_arrow("character") %>
      <%= link_to "Actor", quotes_path(column: "actor",  direction: direction), class: "underline" %>
      <%= filter_arrow("actor") %>
      <%= link_to "Reset filter", quotes_path, class: "underline" %>
    </div>
    <div id="quotes" class="divide-y dark:text-slate-700 mb-10">
      <% @quotes.each do |quote| %>
        <div class="py-3">
          <%= render quote %>
          <%= link_to "View quote", quote_path(quote), class: "text-underline inline-block my-3", data: { turbo: false } %>
        </div>
      <% end %>
    </div>
  <% end %>
</div>

There are helpers embedded in this view that help render the correct UI:

# app/helpers/quotes_helper.rb

module QuotesHelper
  def direction
    params[:direction] == "asc" ? "desc" : "asc"
  end

  def filter_arrow(column)
    if params[:column] == column
      if params[:direction] == "asc"
        "&#8659;".html_safe
      else
        "&#8657;".html_safe
      end
    end
  end
end

And each quote partial has the following code:

<!-- app/views/quotes/_quote.html.erb-->
<div>
  <p>
    <strong>Quote:</strong>
    <%= quote.quote %>
  </p>

  <p>
    <strong class="<%= "text-blue-500" if params[:column] == "character" %>">Character:</strong>
    <%= quote.character %>
  </p>

  <p>
    <strong class="<%= "text-teal-500" if params[:column] == "actor" %>">Actor:</strong>
    <%= quote.actor %>
  </p>

  <%= turbo_frame_tag dom_id(quote) do %>
    <%= link_to "Edit quote", edit_quote_path(quote), class: "underline" %>
  <% end %>
</div>

I also adjusted the _form.html.erb partial slightly.

<!-- app/views/quotes/_form.html.erb-->

<%= form_with(model: quote) do |form| %>
  <% if quote.errors.any? %>
    <div style="color: red">
      <h2><%= pluralize(quote.errors.count, "error") %> prohibited this quote from being saved:</h2>

      <ul>
        <% quote.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div>
    <%= form.label :quote, style: "display: block" %>
    <%= form.text_field :quote, class: "border px-2 py-1 rounded" %>
  </div>

  <div>
    <%= form.label :character, style: "display: block" %>
    <%= form.text_field :character, class: "border px-2 py-1 rounded" %>
  </div>

  <div>
    <%= form.label :actor, style: "display: block" %>
    <%= form.text_field :actor, class: "border px-2 py-1 rounded" %>
  </div>

  <div>
    <%= form.submit class: "underline" %>
  </div>
<% end %>

Here is what's happening:

  • Each filter link in the index.html.erb file has a parameter that gets passed. The parameter maps to the same name as the Quote database column. If we pass that name to the controller via interpolation, we can dynamically sort the records by clicking the link.
  • The helpers introduced help toggle between ascending and descending order and display an arrow to help visually determine the current order.
  • The entire block of code is wrapped in a turbo frame tag, thus hijacking the request from each link. The result is instantaneous UI changes and a nice way to edit a quote in real-time inline without needing to visit a different page, as most legacy rails applications would do.
  • The "Edit" link is wrapped inside the same turbo_frame_tag as the _form.html.erb partial is wrapped in. This signifies to Rails that when that link gets clicked, the same content of the _form.html.erb partial should be piped "over the wire" and injected inside the page.

You might be asking yourself, is that it? And yes, it is. This guide isn't the most exciting, but it's super simple and a quick win to add traditional CRUD-style principles to your apps that feel like single-page apps that used to require all types of JavaScript and configuration.

As this series progresses, we'll tackle more real-world problems. I'm excited to continue!

Link this article
Est. reading time: 6 minutes
Stats: 2,090 views

Categories

Collection

Part of the Hotwire and Rails collection

Products and courses