Globally Disable Buttons on Form Submit

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Log back in... and head to any product page. Thanks to the work that we did earlier, when we submit the review form, the opacity does go lower while the frame is loading. You can see this fairly well on the button... but it is still a bit subtle. So here's an idea: what if we also disabled this submit button while the frame was loading? That would give us an even better loading indicator and, as a bonus, it would help prevent double submits. The best part? We can make this happen for every form on our site by leveraging an event that Turbo dispatches.

Listening to turbo:before-submit

In your editor, open up assets/turbo/turbo-helper.js. Anywhere in the constructor, listen to a new event: document.addEventListener('turbo:submit-start'). Pass this an arrow function with an event argument. Inside, let's console.log() the string submit-start and also the event object.

... lines 1 - 8
document.addEventListener('turbo:submit-start', (event) => {
console.log('submit-start', event);
})
... lines 14 - 91

Turbo triggers this turbo:submit-start event whenever any form is submitted with turbo, whether it's inside of a Turbo frame or just a normal form that Turbo Drive is handling.

Let's go see if this works. Move over, refresh, submit, and go check the console. There it is!

Now some Turbo events have a detail key inside them with extra info. And this is one of those events. This formSubmission key holds all kinds of information about the form submit that's about to start. Most importantly, for us, it has a submitter key set to the button that triggered the submit. That's this button right here!

This is awesome because we can use that to add a disabled attribute! The path to this is detail.formSubmission.submitter.

Disabling the Submitter Button

Head back to our code and replace the log with event.detail.formSubmission.submitter. Add the disabled attribute with .toggleAttribute('disabled', true).

... lines 1 - 8
document.addEventListener('turbo:submit-start', (event) => {
event.detail.formSubmission.submitter.toggleAttribute('disabled', true);
})
... lines 14 - 91

When you use toggleAttribute with a second argument of true, it means:

I want you to add this attribute... but I don't need it to be disabled="something". I just need the disabled attribute.

Let's try that. Refresh the page... and then inspect the button element. Watch it when I click. Yes! Perfect! For just a moment, it had a disabled attribute, which made it even more obvious that it was loading. And, we can't click to submit it twice.

Behind the scenes, our code added the disabled attribute. Then, when the frame finished loading, the entire contents of the frame were replaced with a new, non-disabled form to give us the exact effect we want.

Fixing Disabled Forms in Turbo Snapshots

Scroll up, log out, then go to the registration form. This form does not live in a Turbo frame. But it still gets the new submit behavior! Yup, with just a few lines of code, every form on our site just got a little fancier.

But... there is one... super edge case. If you submitted the form and navigated away from the page while the form was still submitting, that would cause Turbo to take a snapshot of the page with the disabled button. If the user then clicked back on their browser, the button would still be disabled.

This is probably such a rare edge case that... maybe we don't care. But let's code for it.

Back in turbo-helper.js, create a new variable: const submitter =. Copy the event.detail line from below, paste here, and just use submitter below.

We're doing this so we can also give this button a new class: submitter.classList.add('turbo-submit-disabled').

... lines 1 - 9
document.addEventListener('turbo:submit-start', (event) => {
const submitter = event.detail.formSubmission.submitter;
submitter.toggleAttribute('disabled', true);
submitter.classList.add('turbo-submit-disabled');
})
... lines 17 - 101

This class doesn't do anything and doesn't have any CSS attached to it. I just invented it as a way to mark that this button was disabled because of our loading logic.

Why is that helpful? Above this, we're listening to turbo:before-cache. This is called right before Turbo takes a snapshot of the page. We can use the turbo-submit-disabled class to find the disabled button and remove that attribute.

But let's not put the logic here: let's call a new function: this.reenableSubmitButtons().

Copy that method name, scroll all the way to the bottom and paste to create it. Inside, use document.querySelectorAll() to find any element with the turbo-submit-disabled class that we added. Foreach over this, pass a callback with a button argument, and then say: button.toggleAttribute('disabled', false). Fully clean things up by removing the class: button.classList.remove('turbo-submit-disabled').

... lines 1 - 4
document.addEventListener('turbo:before-cache', () => {
this.closeModal();
this.closeSweetalert();
this.reenableSubmitButtons();
});
... lines 10 - 90
reenableSubmitButtons() {
document.querySelectorAll('.turbo-submit-disabled').forEach((button) => {
button.toggleAttribute('disabled', false);
button.classList.remove('turbo-submit-disabled');
});
}
... lines 98 - 101

It's pretty hard to actually repeat the edge case we just fixed... but let's at least make sure we didn't break anything. Submit the form. Yup! That still looks great!

Next: there's another place that we can leverage a Turbo Frame to do something cool. While viewing a product, if we're an admin, it would be awesome to be able to click an "edit" button that would Ajax load the "product form" right into this space. So... let's do it!

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.4.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.13.1
        "doctrine/doctrine-bundle": "^2.2", // 2.3.2
        "doctrine/orm": "^2.8", // 2.9.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^6.1", // v6.1.4
        "symfony/asset": "5.3.*", // v5.3.0-RC1
        "symfony/console": "5.3.*", // v5.3.0-RC1
        "symfony/dotenv": "5.3.*", // v5.3.0-RC1
        "symfony/flex": "^1.3.1", // v1.13.3
        "symfony/form": "5.3.*", // v5.3.0-RC1
        "symfony/framework-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/property-access": "5.3.*", // v5.3.0-RC1
        "symfony/property-info": "5.3.*", // v5.3.0-RC1
        "symfony/proxy-manager-bridge": "5.3.*", // v5.3.0-RC1
        "symfony/runtime": "5.3.*", // v5.3.0-RC1
        "symfony/security-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/serializer": "5.3.*", // v5.3.0-RC1
        "symfony/twig-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/ux-chartjs": "^1.1", // v1.3.0
        "symfony/ux-turbo": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.0-RC1
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.2
        "symfony/yaml": "5.3.*", // v5.3.0-RC1
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/intl-extra": "^3.2", // v3.3.0
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.3.0-RC1
        "symfony/maker-bundle": "^1.27", // v1.31.1
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/stopwatch": "^5.2", // v5.3.0-RC1
        "symfony/var-dumper": "^5.2", // v5.3.0-RC1
        "symfony/web-profiler-bundle": "^5.2", // v5.3.0-RC1
        "zenstruck/foundry": "^1.10" // v1.10.0
    }
}