Auto-Submitting Forms
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeDay 12 already? Over the next 3 days, we're going to work on one, big goal: transforming this table into a rich data-table setup, with searching, column filtering, pagination, all happening with beautiful AJAX. This is one of the parts I'm most excited to dive into.
Our homepage does have a search. And there's nothing particularly special about it. I hit enter to submit the form, the query parameter is in the URL, and it filters the results. Naturally, thanks to Turbo Drive, it all happens via AJAX.
For our first trick, watch as we make the search update automatically as we type. So we type and, without hitting enter, the list should update.
To do this, we're going to borrow a controller from a 30 Days of Hotwire repository. This comes from a fantastic 30 Days of Hotwire challenge that someone from the Rails community did. I love this series and it has a ton of good stuff. I highly recommend checking it out.
The autosubmit Stimulus Controller
Anyway, I'm going to borrow this great "auto-submit" controller. It's dead-simple: it gives us a way to submit a form... with optional debouncing. If I type really quickly, I don't want to submit the form four times. I want it to wait for a slight pause... and then submit. That's called debouncing. This waits for a 300 millisecond pause.
So let's roll up our sleeves and get this into our app. Create a new file called autosubmit_controller.js
... then paste:
import { Controller } from "@hotwired/stimulus" | |
import debounce from 'debounce' | |
// Connects to data-controller="autosubmit" | |
export default class extends Controller { | |
initialize() { | |
this.debouncedSubmit = debounce(this.debouncedSubmit.bind(this), 300) | |
} | |
submit(e) { | |
this.element.requestSubmit() | |
} | |
debouncedSubmit() { | |
this.submit() | |
} | |
} |
Then head to the homepage to use it. Near the top... here's our search form. On the form, add data-controller"autosubmit"
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="flex"> | |
// ... lines 7 - 13 | |
<section class="flex-1 ml-10"> | |
<form | |
// ... lines 16 - 18 | |
data-controller="autosubmit" | |
> | |
// ... lines 21 - 30 | |
</form> | |
// ... lines 32 - 85 | |
</section> | |
</div> | |
{% endblock %} |
Notice I'm getting auto-complete on that. That's thanks to a Stimulus plugin I have for PhpStorm.
Next, down on the input, say data-action
equals autosubmit#debouncedSubmit
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="flex"> | |
// ... lines 7 - 13 | |
<section class="flex-1 ml-10"> | |
<form | |
// ... lines 16 - 18 | |
data-controller="autosubmit" | |
> | |
<input | |
// ... lines 22 - 27 | |
data-action="autosubmit#debouncedSubmit" | |
> | |
// ... line 30 | |
</form> | |
// ... lines 32 - 85 | |
</section> | |
</div> | |
{% endblock %} |
In the controller, you can call submit
to submit the form immediately or debouncedSubmit()
to wait for the pause. And we don't need to include the event name this time - like input->
to listen to the input
event. When you apply a data-action
to an input
, a button
or a link
, Stimulus figures out which event you want to listen to. Most of the time, life will be simple like this.
Installing the Missing Package
Does it work? No! Because we have an error... an error that I hope will feel familiar!
Failed to resolve module specifier
debounce
.
This comes from our code! Our copied code is using a debounce
package... and we don't have that installed! Cool! Copy debounce
, spin over and run:
php bin/console importmap:require debounce
Now it's in our project... and now the error is gone. Ready for the magic? Hey, it's working! Just one request after I finished typing thanks to debounce!
The only bummer is that we're losing focus when it reloads the entire page. As a workaround - this is not going to be our final solution - we can try putting autofocus
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="flex"> | |
// ... lines 7 - 13 | |
<section class="flex-1 ml-10"> | |
<form | |
// ... lines 16 - 18 | |
data-controller="autosubmit" | |
> | |
<input | |
// ... lines 22 - 28 | |
autofocus | |
> | |
// ... line 31 | |
</form> | |
// ... lines 33 - 86 | |
</section> | |
</div> | |
{% endblock %} |
This... almost works... except we're losing the cursor location: it puts us back at the beginning. That's okay: we're going to solve this in a much better way soon. And when we do, we're not even going to need the autofocus.
Tomorrow, let's make this richer by adding pagination and column sorting.
This would be a better idea:
autofocus
onfocus="this.setSelectionRange(this.value.length, this.value.length)"