Building a (Very) Simple Responsive Search with Rails & Stimulus
Here's a simple and responsive search form I put together for a recent side project, using Hotwire's Stimulus framework and Rails with Turbo.
The Form
Here's how the search input looks. It's a simple search form connected to a Stimulus controller. The input triggers the search function upon every input event. The debounce logic occurs in the Stimulus controller to make sure not too many requests are made to the server:
<%= form_with url: items_path, method: :get,
data: { controller: "search", turbo_frame: "items_list" } do |f| %>
<%= f.text_field :query,
placeholder: "Search...",
data: {
search_target: "input",
action: "input->search#search"
} %>
<button type="button"
class="hidden"
data-search-target="clearButton"
data-action="click->search#clear">
×
</button>
<% end %>
There's an additional feature here: a button which appears in the search field whenever the field has a value. The button has a data-action attribute pointing to the clear action. If the button is clicked, the input value is cleared out.
The Stimulus Controller
All we need to do here is submit the form with a debounce timer, handle the action for clicking the clear input button, and handle hiding the clear button if the input field has a value or not.
export default class extends Controller {
static targets = ["input", "clearButton"]
connect() {
this.toggleClear()
}
search() {
this.toggleClear()
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.element.requestSubmit()
}, 300)
}
clear() {
this.inputTarget.value = ""
this.toggleClear()
this.element.requestSubmit()
}
toggleClear() {
if (this.inputTarget.value.length > 0) {
this.clearButtonTarget.classList.remove("hidden")
} else {
this.clearButtonTarget.classList.add("hidden")
}
}
}
The Turbo Frame
You'll notice the form targets an items_list turbo frame, which contains your search results in the view. When the Stimulus controller submits the form, the content inside this frame gets replaced with the new search results. This is a pretty simple way to update the search results without full page reloads. I also wanted to avoid turbo streams for this implementation, because I find them a tad awkward sometimes.
<%= turbo_frame_tag "items_list" do %>
<% @items.each do |item| %>
<%= render item %>
<% end %>
<% end %>
The Rails Controller
Standard index action with optional query filtering. Works with or without search params:
def index
# you will probably paginate your results
if params[:query].present?
@items = Item.where("name LIKE ?", "%#{params[:query]}%")
else
@items = Item.all
end
end
Conclusion
This is a nice simple starting point to build out some more complicated search features from. I'd recommend using a pagination gem like pagy to handle the search results while keep your view manageable.