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"
"⇓".html_safe
else
"⇑".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 theQuote
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!
Categories
Collection
Part of the Hotwire and Rails collection
Products and courses
-
Hello Hotwire
A course on Hotwire + Ruby on Rails.
-
Hello Rails
A course for newcomers to Ruby on Rails.
-
Rails UI
UI templates and components for Rails.