Dev

Hotwire: Reactive search form without JavaScript

Profil Picture

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.

  1. Make it fast ⚡️
  2. Update the url and the history to allow users to share filters
  3. Without page reloads (obviously)
  4. 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.

AJAX requests when I start typing
AJAX requests when I start typing

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 src is automatically filled by the form when submitted.
The src is automatically filled by the form when submitted.

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.

Simplify your time tracking with Timecop

Timecop is a time tracking app that brings simplicity in your day to day life.

Timecop projects