Building an instant search form with Stimulus.js and Rails15 Nov 2020
August 5, 2021 update: Since publishing this post, Turbo was released. While the method described here for building a live search interface with Stimulus still works just fine, if you’re using Stimulus, you might be interested in the other half of Hotwire on the web. In August of 2021, I published a post describing how to build the same interface described here with hotwire-rails, instead of just Stimulus.
When we’re finished, we’ll have a list of Players that users can search by name. As they type in the search box, we’ll make a request to the server for players that match and display the results on the frontend (almost) instantly. Here’s what the end result will look like:
I’m also not going to do a full overview of how Stimulus works, since the official docs do that well. Instead, our Stimulus work will be focused on building the desired behavior in Stimulus and connecting our Stimulus code to our HTML.
I’m going to walk through this step-by-step from a new Rails 6 project. If you want to follow along from an existing project that already has Stimulus and rails-ujs installed, skip slightly ahead to the Adding a search form section.
Let’s get started.
Setting up our project
First, we’ll create our Rails project, specifying that we want to include Stimulus:
Then, we’ll create our database and scaffold up a Player resource that our users will be soon be searching against:
That’s it — that’s all the setup we need to do. Now we can dive into the fun stuff.
Adding a search form
Our users will need a search form if we want them to be able to search. We’ll first create a partial for our search form:
And then update app/views/players/index.html.erb to render the search form:
And add our player partial, which we’ll use later to render our search results:
Our search form partial looks like this:
You’ll notice that our search form url references a controller that we haven’t created yet, the
While we could build a search method in our
PlayersController creating a separate controller lets us keep our controller actions RESTful and is a little more Rails-y, in my opinion.
Let’s add our controller now:
Notice the layout false which ensures that our index view will only render the HTML in
app/views/player_search/index.html.erb. If we don’t set layout to false, every search request we make will render our index view inside of the layout defined in
app/views/layouts/application.html.erb — we don’t want that since we’re planning to insert the search results into a piece of an existing page.
Next we’ll add the content of our
PlayerSearch index view. Our view simply loops over an array of players and renders the player partial each iteration.
Finally, we’ll update our routes file so that we can access thePlayerSearch index method.
Now that we have the search form rendering and the controller action built, our next step is to implement the Stimulus search controller.
The Stimulus controller
First, let’s create our Stimulus controller:
This controller has two jobs — submitting the search form as the user types and handling the response from the server each time a search query completes.
Let’s implement form submission first, in a function we’ll call
Rails.fire is, a poorly documented function available through rails-ujs. It takes an element (formTarget, in this case, which we’ll attach add to our HTML shortly) and the name of an action to trigger (like click or submit) and then triggers that event on the element. Despite the poor documentation around this method, it is the preferred method of submitting forms programmatically in Stimulus controllers.
Now we can submit the search form programmatically.
Next, let’s add the function to handle search results from the server. We’ll do this with a new function in our Stimulus controller:
This function grabs the inbound response from the server and replaces the contents of a (yet-to-be-defined) DOM element with the portion of the response we care about.
When finished, our complete Stimulus controller looks like this:
Connecting the controller to the frontend
The Stimulus controller is ready — our last step is to connect our Stimulus controller and our HTML. To do so, we’ll need to update the
index view in our Players view and the
search_form partial we created earlier.
First, the updates to the the index view:
The data-controller attribute added to our parent div connects our Stimulus controller to the DOM. The data-target attribute on the results container div is used by handleResults in our Stimulus controller to render the results of our search when a request is submitted. If any of this syntax is confusing, the best place to start is the Stimulus handbook which explains how Stimulus uses these data attributes in detail.
Last step before we can start typing search terms — updating our search form partial:
We’re doing a few things here, let’s walk through it.
data-action on our form element calls our
handleResults method each time this form is submitted successfully. The
data-target on the form element gives us an easy reference to the form in the Stimulus controller which we use in the search function to submit the form.
On the text field, we add a
data-action that calls the
search method each time the input event is fired on this input. Because using the browser’s autocomplete functionality on a search form isn’t usually desired, we also turn off autocomplete. You can skip turning off autocomplete if you like, it won’t bother me.
That’s it — as users type in the search field we automatically send a request to the server to find matching players and update the players list so users can see results in real time. Add a few players to your database and try it out!
Improving our search method
As you followed along with this tutorial, you might have noticed that our implementation submits the form every time the input event fires. This means that if the user types 10 characters in a row we’ll make 10 separate search requests to the server. In a toy application like ours that’s okay, but we can do a little better.
Instead of submitting the form each time an input event fires, we can instead wait for the user to stop typing before submitting the form. One way of doing this is by using a debounce function to delay form submission until after the user has stopped an action for a period of time. A simple implementation of debouncing that makes use of setTimeout and clearTimeout looks like this:
This change to delay the Rails.fire call until 200ms after the last input event fires.
In a real world application, instead of implementing your own debounce function, you should consider a resilient, well-architected solution from a library like Lodash. You can read more about debouncing here.
Together we’ve built an instant search results with a very small amount of Stimulus and a few data attributes in our HTML. What’s really cool about this implementation is that it is reusable anywhere in your application that you want the same behavior. Since the Stimulus controller just submits a form and replaces data on the page, you could easily reuse the exact same Stimulus controller to build a search UX for any resource in your application!
From the base we’ve built in this demo application, we could add more complex search and filtering options and implement loading states to improve user experience on slower requests.
Thanks for reading!