Turbo (part of Hotwire) is a supercharged version of Turbolinks. If you’re used to immediately turning off Turbolinks in any new project, you might be surprised to learn that you don’t need React or Vue to build a rich and interactive web app.
What We’re Building
We’re going to be adding a single page search to a simple Single Page App (SPA) that allows users to view information about some Movies.
The starting point is heavily based on ‘Using Hotwire with Rails for a SPA like experience’ by Mike Wilson. If you want to follow along with this post you can use the starting point here on Github or follow along through that post. You should have a simple app where you can select a movie without reloading the page, with only a very simple rails controller.
The first step is to add search support to the controller.
We can verify this is working by simply adding the
query parameter to
the URL and visiting the page again. Only the movies matching the query you
made are present in the list.
Not very user friendly though. Let’s add a search form to the top of the list
of movies, within the
:index - the same turbo-frame
with the list of movies.
Now when you make a search and hit enter, the list of movies is filtered. You’ll also notice that, if you have a movie selected, it stays selected. Searching doesn’t cause a full page refresh, only the list of movies updates.
This happens because each turbo-frame creates its own navigational context.
By default all links and form submissions from within a given turbo-frame only
effect that turbo-frame. Under the hood, turbo is hijacking the submission
event, making an AJAX call for the form submission and interpreting the result.
When the submission is successful turbo reads the response, looking for a
turbo-frame-tag that has the same name as the turbo frame that caused the
submission. If it finds a match, it swaps out the contents.
In our case, the form submission returns the same page so it is pretty clear
that there will be a matching turbo-frame - the
I said this was the default behaviour. By adding a
to a form or link turbo will instead look for, and swap the contents in, the
turbo frame with the name you passed - this is how clicking the movies changes
:details turbo frame and not the
Come Alive with Stimulus
We can watch for input changes on the text field of the search form and call an action on a Stimulus controller to submit the form for the user. This can be achieved with a small amount of markup on the form and the text field.
On the text field we’ve added
The value assigned to the
data-action attribute encodes which event to listen
for and which controller and action (method) to invoke when the event happens.
The generic markup is
event->controller-name#action. In our example,
submit-form and our
input event name comes from the JS DOM events, and we
want to listen to any changes in the input. The controller name
is just the name we gave the controller we want to invoke here - it is expected
to be defined in a file named
submit_form_controller.js. Its name in
is just a public method defined on that controller.
On the form element we’ve added
data-submit-form-target="form". The former simply tells Stimulus to attach
a new instance of the
SubmitFormController to the form element, allowing us
to invoke its actions from the form element or any of its children. The
latter attribute makes the form element available from the controller as a
form - from the controller we will be able to access the
element by calling
SubmitFormController is very simple.
submit action is called it submits the target, which is assumed to
be a form. The action itself is debounced. This stops us from generating too
many simultaneous requests which makes the submissions overlap.
this.formTarget.requestSubmit(); is very important, if we used
requestSubmit(), the event that Turbo hooks into the intercept the
form submission would not happen and you would have a normal form submission -
with a full page refresh.
A Small Problem
This works really well - until the first time the form is submitted - then the focus in the text field is lost and we can’t keep typing. That’s definitely a bug!
What’s happening here is that when the response is returned, we’re replacing the whole
contents of the
:index turbo frame - including the search form itself. To avoid this
we can move the search form outside of the turbo frame, and then add
turbo-target: :index to the form element. This will mean when the form is submitted,
turbo will replace the contents of the
:index turbo-frame and leave the search field
The resulting code can be found on Github.