Instant search with Rails 6 and Hotwire05 Aug 2021
Last year I wrote an article on building an instant search form with Rails and StimulusJS. Since then, Turbo, the other half of Hotwire for the web has been released. Turbo opens the door for an even simpler, cleaner implementation of an instant search form.
So, today we’re taking another look at how to build a search-as-you-type interface that allows us to query our database for matches and update the UI with those matches (almost) instantly.
The finished product will work like this:
I’m writing this assuming that you’re comfortable with Ruby on Rails. You won’t need any knowledge of Hotwire (Turbo or Stimulus) to follow along. Those that will get the most value from this guide will likely be newer to Hotwire, looking for an example of how to build a fairly common user experience with Hotwire.
You can find the complete code for this guide on Github.
Let’s get started.
First, we’ll need an application with both Turbo and Stimulus installed and ready to go. If you’ve already got an application setup, feel free to use that instead.
To follow along line-by-line, start by running the below commands from your terminal. Your machine will need to be setup for standard Rails development, with Ruby, Rails, and Yarn installed.
For reference, I’m writing this guide using Rails 6.1 and turbo-rails 7.0.0-rc.1.
With the above commands run, we should have a new rails application up and running, and visiting http://localhost:3000/players should open a page that lists the Players in your database and allows you to add and remove players.
Go ahead and add a few new players from the UI, or open up your Rails console and add some players there. Give them different names since we’ll be filtering the list of players by name in this project.
Add the search form and controller action
With setup complete, open up the code in your favorite editor.
We’ll start by updating the players index view to add a form that we’ll use to filter the list of players. Update the players index view to add the form:
Since we’ve added a
form_with that expects a route named
search_players_path to exist, we’ll update our routes file to add that path:
One important thing to note is that because we are going to respond with a turbo stream, our form needs to send a POST request, not a GET. There are ways to work around this behavior but instead of using workarounds, we’ll just use a POST request for our search form.
You can read more about the reasons for this behavior, and find possible workarounds, on Github.
While we could add a dedicated controller for searching, in this case we’ll keep things simple by just adding the route to the existing
PlayersController. With the path added, next let’s update the
PlayersController to add our search method:
We won’t win any awards for the elegance of our database query, but the important part of the method is the
Rather than a standard HTML response, requests to players/search will send back a turbo_stream response that targets an element on the page with the id of
players and replaces the contents of that element with a
Responding with a turbo_stream instead of HTML results in a payload sent to the browser that will look something like this:
Note that this is still HTML, just wrapped in the special
<turbo-stream> element and sent with a header (
text/vnd.turbo-stream.html; charset=utf-8) that indicates the response is a turbo stream.
When Turbo sees a response with the turbo-stream header and a turbo-stream wrapped HTML fragment, it reads the
action and the
target (or, newly added),
targets from the
<turbo-stream> and uses that to update only the relevant part(s) of the DOM.
Add a stream target
Now we know a little bit about what’s happening behind the scenes with Turbo, but none of this will work in our application yet. We’re responding to form submissions with a partial that we haven’t created yet and our form can’t be submitted since we don’t have a submit button.
Let’s dive back in to the code by creating our list partial. From your terminal:
And then fill that partial in with:
id attribute on the
<tbody> element. This id must match the target passed to
turbo_stream.replace in our controller, otherwise nothing will happen when we search. The rest of this partial is just boilerplate generated by the Rails scaffold.
Finally, update the players index view to use the list partial we just created:
Submit the search form
With our list partial in place, our last step is to automatically submit the search form as the user types. To do that, we’ll create a small Stimulus controller and attach it to our search form. First, in your terminal:
And fill that controller in with the below:
search function waits for the user to stop typing and then submits our form. To trigger the form submission, we’re borrowing a technique outlined in Better Stimulus.
With the Stimulus controller added and the list partial created, the last step is to add the Stimulus controller to our form by updating our search form in the players index view as follows:
Here we’re in regular Stimulus land. We add data attributes to the
<form> element to connect our controller and set the necessary form target and then the text field is updated with a data-action attribute that triggers the
search function in the
form-submission controller on input.
With this last piece in place, load up /players in your browser, start typing, and you should see the list of players update automatically, like this:
This small example should help you get a taste for how simple it can be to build modern, highly-responsive applications with Ruby on Rails and Hotwire, combining the feel of a SPA with the developer experience that the Rails world (usually) loves.
To use this in the real world, we’d need to think about things like:
- A loading state: Replacing the players
<tbody>on when form submission starts could be a good starting point
- An empty state: The list partial could render different content when
- Cleaner, more performant database queries: Definitely don’t just leave your query sitting in the controller! For production use cases, you’d want to consider an option like pg_search
While this example is not yet production-ready, I hope it gives you a good starting point into the world of Hotwire-powered Rails applications. The next time you’re building something new, consider whether Rails + Hotwire might be enough to deliver the experience your users expect while keeping your team happy and productive.
As always, thanks for reading!