Using StimulusJS for Type-ahead Search (2/2)

The Rails Side

I’ve recently started using StimulusJS as the web framework for a Rails 5.2 application. In Part 1 I built a simple type-ahead search form. In this second half I walk through using a controller and partials to render the results, just like a traditional Rails app.

The Router & Controller

In Part 1, the StimulusJS code we created fetches a ‘query.html’ page in our Rails app:
fetch('/search/query.html?search='+ this.searchItemTarget.value)
So in my search_controller.rb, I have a query method:
def query
  @search_param = params[:search].to_s.downcase
Here I’m taking the text from the input field (this.searchItemTarget.value) that was put after ‘“?search=” in the URL and assigning it to my @search_param instance variable. I’m also making sure I’m getting a string (to_s) and making it all lower-case letters (downcase). Of course I’ve also declared the route my routes.rb:
  get 'search/query'
If I check my server console screen or my development.log, I should now see the search page accessing the query page every time I type a key:
Started GET "/search/query.html?search=N" for 
Processing by SearchController#query as HTML
Parameters: {"search"=>"N"}
Rendering search/query.html.erb
Started GET "/search/query.html?search=Ne" for
Processing by SearchController#query as HTML
Parameters: {"search"=>"Ne"}
Great! Now let’s build some search results.

Building the Search

First we have to get the results of our search. Of course, you’ll be querying whatever ActiveRecord model you want to use:
@search_string = '%' + @search_param.gsub(" ","%") + '%'
@names = User.where('LOWER(name) like ?', @search_string)
I’m doing a couple of things here:
  1. I’m taking the text being queried and transforming it into a wildcard search. That way, if someone searches for ‘M J’, they’ll get ‘Mary Jane’, ‘Michael Jackson’, and ‘Marjorie Willis’ and even ‘Sammy Davis Jr.’
  2. Of course, I’ve already made the search query lower case, so I have to make what I’m searching lower case as well to match. ‘LOWER’ is the Postgres SQL command to do that on the fly and much more efficient than calling each record in Rails. If you’re not using Postgres, you’ll have to check your database’s documentation.
As was mentioned in part 1, with StimulusJS I’m returning the search results as HTML so I can keep all my html in traditional erb templates. If I didn’t add anything else to my controller, it will return the erb file query.html.erb, but it will also return my default layout and references to all the Javascript and CSS that’s already rendering in the current application. To fix that, I want to render the results with layout:false so Rails knows I just want the html within the html document I just created. Here’s the final version of the controller action:
def query
  @search_param = params[:search].to_s.downcase
  @search_string = '%' + @search_param.gsub(" ","%") + '%'
  @names = User.where('LOWER(name) like ?', @search_string)
  render layout:false

The View/HTML

So now we just need to create our query.html.erb. I’m just going to output the results as a common unordered list:
  <% @names.each do |name|
  <li><%= name.first_name %> <%= name.last_name%></li>
  <% end %>
Easy enough! If I wanted to get fancy, I could even extract the list to a _search_list.html.erb partial so I could reuse the html. Why would I want to reuse it? Well, if the user of our little search form just hits ‘<enter>’ (or we add a submit button and they click it) they’ll end up submitting the form, which we’ve pointed to a ‘results’ page. Instead of having to write that page from scratch I can share the partial between the two, something like:
<div class="results-card">
  <render partial: 'search_list', locals: {names: @names}


So with a lot of CSS and a slightly more complicated unordered list, I was able to use StimulusJS and Rails to create quite a nice little type-ahead form:
While I may very well run into some issues later as my layouts get more complex, for now I’m really enjoying the ease-of-use of StimulusJS, how well it was able to fit into the stack, and how quickly I was able to be productive with it.