Hotwire: Reactive search form without JavaScript
Guillaume Briday
3 minutes
You don't need complicated yet fancy front-end frameworks like React or Vue to create reactive index. Let's see how we can do it with Hotwire, a dead simple <form>
and the new <turbo-frame>
concept.
What we want to do
Most Web applications have index with search box or filter options. We would like to create reactive index with multiple things to make the UX perfect.
- Make it fast ⚡️
- Update the url and the history to allow users to share filters
- Without page reloads (obviously)
- Keep the UI in sync (pagination, number of items, clicks on buttons)
Getting started
Let's start with the basic stuff, shall we?
We have our classic controller like you probably already have in your application.
I use Pagy to create paginated results. I also added a search
scope in my Todo
model.
class TodosController < ApplicationController
def index
@pagy, @todos = pagy Todo.search(params[:q]).order(created_at: :desc)
end
end
class Todo < ApplicationRecord
scope :search, ->(query) {
return if query.blank?
where(arel_table[:description].matches("%#{query}%"))
}
end
Now here comes the fun! We need to tweak our view a bit to add some turbo-frame
related code.
# View: app/todos/index.html.erb
<%= form_for todos_path,
method: :get,
html: {
data: {
turbo_frame: 'todos_list',
turbo_action: 'advance',
controller: 'form',
action: 'input->form#submit'
}
} do |f| %>
<%= search_field_tag :q, params[:q], placeholder: "Search..." %>
<% end %>
<%= turbo_frame_tag 'todos_list' do %>
<%= render partial: 'todos/todo', collection: @todos %>
<%= pagy_nav(@pagy) %>
<% end %>
The most important part of the piece of code is the data-turbo-frame='todos_list'
data attribute.
This attribute allows us the replace the content of a <turbo-frame>
when it's submitted. In this case, the response of the todos_url
endpoint will replace the <turbo-frame>
with the id=todos_list
wherever it is on the page.
The attribute data-turbo-action='advance'
allows us to replace the history with the new URL!
We almost already get the job done and it's crazy to think that is that simple with Hotwire and Rails!
Submit form when typing
To improve the UX, we want to submit the form automatically when the user types something into the inputs.
We need a little spark of JavaScript with Stimulus.
Let's create our controller:
// app/javascript/controllers/form_controller.js
import { Controller } from '@hotwired/stimulus'
import throttle from 'lodash.throttle'
export default class extends Controller {
initialize() {
this.submit = throttle(this.submit, 500) // In ms, you can adapt to your needs
}
submit() {
this.element.requestSubmit() // Programatically submit the form.
}
}
We also throttle
the submit
method to 500ms
to prevent AJAX calls on every input, it will be completely transparent and smooth for the user though.
We can add our data attributes data-controller="form"
and data-action="input->form#submit"
to the form as we did above.
And the content if automatically replaced without page reloads! ⚡️
How it works
The only thing that happened behind the scene is that the data-turbo-frame='todos_list'
data attribute updates the turbo-frame
's src
and it's the responsibility of the turbo-frame
itself to make the AJAX call.
The whole index
page is generated on the server by Rails directly, but only the turbo-frame
is updated on the page. Turbo extracts the content from the response itself to replace it.
Conclusion
In few lines, we have a reactive index with filters, pagination, awesome performances, interactions and events!
No need to:
- Have complicated frameworks and toolchain.
- Have an API and its documentation.
- Configure Axios to make API calls.
- Deploy two apps.
- Rewrite our whole existing application.
It's basic forms, basic HTTP responses, basic server rendered HTML. Easy peasy! 😎
With that in mind, you can do progressive enhancements in your application page after page.
How cool it that?! Let's keep our applications simple and clean.