November 8, 2021

Search modal with Hotwire (Part 1) 🔎

Recently I've been adding fullscreen search modals to all of my Rails apps. It's surprisingly easy to add this functionality using a bit of Turbo, Stimulus and Postgres. I'll show you how you can add that to your Rails app too!

Search modal

I'm using the following tools to create a search modal:

Prerequisites

If your app already supports modals using Hotwire: great! If not, read this article to learn how to add modals to your Rails app.

1. Create a SearchController

Begin by creating a simple controller as a starting point.
class SearchController < ApplicationController

  def new
  end

  def create
    @results = [] # We'll replace this later with actual search functionality
    render :new
  end

end
And add the appropriate route to routes.rb.
resource :search, controller: "search"

2. Search modal view

Create a new view in app/views/search/new.html.erb and create your search form like so:
<turbo-frame id="modal">
  <div class="modal items-start max-h-screen py-6 pt-20" data-controller="modal" data-action="keyup@document->modal#escClose">
    <button type="button" class="cursor-default w-full h-full fixed inset-0 bg-gray-700 bg-opacity-25 animate__animated animate__fadeIn animate__faster" tabindex="-1" data-action="modal#close"></button>
    
    <div class="modal-window animate__animated animate__fadeInDown animate__faster">
  
      <div>
        <%= form_with url: search_path, class: "relative", data: {turbo_frame: :search_results} do |f| %>
          <%= f.text_field :query, placeholder: "Search...", class: "w-full bg-transparent border-none focus:ring-0 text-lg px-4 py-0.5" %>
        <% end %>
        
        <%= turbo_frame_tag :search_results %>
      </div>

    </div>
  </div>
</turbo-frame>
Note that we already have a search_results turbo frame in place to render our results. The search form points to turbo_frame: :search_results to make sure the response is rendered in the :search_results turbo frame.

3. Add results partial

Create a new view partial in app/views/search/_results.html.erb so you can render the search results:
<%= turbo_frame_tag :search_results do %>
  <div class="py-2">
    <% @results.each do |result| %>
      <%= link_to result, class: "block px-2 leading-tight", data: {turbo_frame: "_top"} do %>
        <div class="flex items-center justify-between w-full px-4 py-3 rounded-md bg-gray-400 bg-opacity-0">
          <div>
            <div class="font-medium flex items-center">
              <%= result %>
            </div>
          </div>
        </div>
      <% end %>
    <% end %>
  </div>
<% end %>
Obviously, I'm just rendering result in this example, which can be anything depending on your app. Let your imagination run wild! Make sure that any links use data-turbo-frame="_top", or Turbo will try to load the response inside your turbo frame.

4. Search for something

Let's add some actual code that searches your database. Since this is highly dependent on your app, I'll give you an example of Spina CMS. In Spina Pro you can search for pages, simply by doing Spina::Page.search("some search term"). If you don't have a proper search method yet, you can keep it simple by doing something like this:
YourRecord.where("some_column LIKE ?", "%#{params[:query]}%").limit(10)
A simple LIKE-query actually works really well for many use cases.

Add your search logic to the Search#create action:
# search_controller.rb
def create
  @results = Spina::Page.search(params[:query]).limit(5)
  render :new
end
It looks a bit strange to use render :new here, but Turbo makes sure it only replaces your results Turbo frame.

5. Add a link to your modal

Add a link somewhere in your layout to open your search modal. I like to add a search icon in my navigation bar.
<%= link_to new_search_path, data: {turbo_frame: "modal"} do %>
  <!-- Some search icon, try heroicons.com -->
<% end %>

Try out your search modal!

You should now have a basic, functioning search modal.

You can submit your search form by hitting return. That's not really user-friendly though. Let's add some sprinkles with Stimulus!

Part 2: "Adding sprinkles" coming soon!