Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Form Submit Confirmation Controller

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

Let's add a few items to our cart, like a floppy disk - gotta have those - and maybe also some CD's so I can burn a mixtape for Leanna. Now head to the cart page.

A user can already remove an item from their cart. Open up the template to see how: templates/cart/cart.html.twig. Scroll down a bit... here it is... around line 50. The "remove" button is inside a form. When the user clicks, the form submits and the controller removes the item from the cart. It's super smooth and super boring. I love it!

A Massively Re-usable "Submit Confirm" Controller

But now, I need to enhance this. When the user clicks "remove", I want to open a modal where the user can confirm that they want to remove the item. In fact, this is going to be even cooler than it sounds because the Stimulus controller we're about to create will be re-usable across any form in our entire app. Want to pop up a confirmation before the user submits a checkout form... or change password form? This one controller will be able to handle all of those cases.

Let's get to work. Start by creating the Stimulus controller. In assets/controllers/, add a new file called, how about, submit-confirm_controller.js. I'm calling this submit-confirm... and not "delete-confirm" or "cart-remove-confirm" because it will be reusable on any form.

Start the normal way: import { Controller } from 'stimulus' and then export default class extends Controller with a connect() method to make sure everything is hooked up: console.log()... a dinosaur (🦖).

import { Controller } from 'stimulus';
export default class extends Controller {
connect() {
console.log('🦖');
}
}

Next up, go activate this in the template. Adding it to the form tag should be fine: {{ stimulus_controller('submit-confirm') }}.

... lines 1 - 2
{% block body %}
... lines 4 - 27
{% for item in cart.items %}
... lines 29 - 44
<form
... lines 46 - 50
{{ stimulus_controller('submit-confirm') }}
>
... lines 53 - 57
</form>
... lines 59 - 64
{% endfor %}
... lines 66 - 84
{% endblock %}
... lines 86 - 87

Let's make sure it's connected! I'll re-open my console.. refresh and... roar! We even see two dinosaurs because there are two different controllers on this page. My 4 year old son would be thrilled.

Hello SweetAlert2

To create the actual modal, search for sweetalert2. I love this library. It's an easy - but highly customizable - alert system. If you scroll down a bit... one of these examples is for a modal that confirms deleting something. Here it is. This is almost exactly what we want.

Let's go get this library installed. Spin over to your terminal and run:

yarn add sweetalert2 --dev

Adding the "submit" Action

Before we use that new library, let's set up the action on our form: when the user submits the form, we want to run some code.

In the template, on the form, add data-action="" then the name of our controller - submit-confirm - a # sign and... let's have this call a new method named onSubmit.

... lines 1 - 2
{% block body %}
... lines 4 - 44
<form
... lines 46 - 51
data-action="submit-confirm#onSubmit"
>
... lines 54 - 58
</form>
... lines 60 - 85
{% endblock %}
... lines 87 - 88

Copy that, then head over to our controller. Rename connect() to onSubmit() and give it an event argument. Start by calling event.preventDefault() so that the form doesn't submit immediately. Then let's console.log(event) so we can see this working.

... lines 1 - 2
export default class extends Controller {
onSubmit(event) {
event.preventDefault();
console.log(event);
}
}

Head back over, refresh, hit remove and... awesome! The submit event is being triggered. Nothing can stop us... except, maybe typos!

Now let's bring in SweetAlert. Back over on its docs, copy the entire delete example and, in the controller, remove the log and paste.

Oh and this Swal variable needs to be imported: import Swal from 'sweetalert2';

... line 1
import Swal from 'sweetalert2';
... line 3
export default class extends Controller {
onSubmit(event) {
event.preventDefault();
Swal.fire({
title: 'Are you sure?',
text: "You won't be able to revert this!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, delete it!',
}).then((result) => {
if (result.isConfirmed) {
Swal.fire(
'Deleted!',
'Your file has been deleted.',
'success',
)
}
})
}
}

Yay! Let's try it. Head back over to our site, refresh and hit remove. Tada! That's so cool! If we click cancel, nothing happens. And if we click yes, delete it... we get this other message. But it's not actually removing the item... yet.

Look back at the code. Here's how this works: when you click a button, the .then() callback is executed. That's why we saw that second message: on confirm, it called Swal again.

To make this actually submit, replace the Swal.fire() with this.element - which will be the form - .submit().

... lines 1 - 4
onSubmit(event) {
... lines 6 - 7
Swal.fire({
... lines 9 - 15
}).then((result) => {
if (result.isConfirmed) {
this.element.submit();
}
})
}
... lines 23 - 24

That's it! Oh, and if you're thinking:

Hey! Won't this cause an infinite loop... where we call submit() and that causes a submit event... that triggers our submit action... which will then open SweetAlert again?

Fortunately... that will not happen. When you call .submit() on a form element, the form does submit, but the submit event is not dispatched. And so, our action method will not be called again. That's just how JavaScript and the DOM work - not a Stimulus thing. I say that a lot.

Anyways, let's see if this works! Refresh, click remove and this time confirm. Woohoo! The form submitted, the page reloaded, and the item is gone!

But I think we can make this even more awesome. How? By making our controller configurable - like the text that it displays - so we can truly reuse it anywhere in our app. Even in this situation, saying "yes, delete it" on the button... when you're actually removing an item from a cart... it doesn't really make sense.

And as an extra bonus, we're going to add an option to make the form submit via Ajax. That's all next.

Leave a comment!

4
Login or Register to join the conversation
Alexandre C. Avatar
Alexandre C. Avatar Alexandre C. | posted 2 years ago

Hello !

Thank you for this tutorial.
Is there a way to combine the {{ form_start }} with the {{ stimulus_controller }} ? In order to add a data-controller on the form tag.

Have a good day,

Alexandre

Reply

Hey Alex!

Yes, that's totally doable. You just need to specify the attr property to the form_start function. Something like this


{{ form_start(form, {
    attr: {
        'data-controller': stimulus_controller('submit-confirm')|replace('data-controller': '' }
    }
}) }}

The trick here is to remove the string part "data-controller" from the output of stimulus_controller()

Cheers!

Reply
Alexandre C. Avatar
Alexandre C. Avatar Alexandre C. | MolloKhan | posted 2 years ago

Thank you for this answer.

Unfortunately, when I want to pass multiple values to the controller, this is kind of dirty with all the "replace" filters.
It's not a real problem because I can do it with the simple <form> html tag.

Have a good day !

Reply

yeah, it's not a clean approach but you could create your own Twig function to hide and reuse the replacing functionality

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works perfectly with Stimulus 3!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.18.5
        "symfony/form": "5.2.*", // v5.2.3
        "symfony/framework-bundle": "5.2.*", // v5.2.3
        "symfony/property-access": "5.2.*", // v5.2.3
        "symfony/property-info": "5.2.*", // v5.2.3
        "symfony/proxy-manager-bridge": "5.2.*", // v5.2.3
        "symfony/security-bundle": "5.2.*", // v5.2.3
        "symfony/serializer": "5.2.*", // v5.2.3
        "symfony/twig-bundle": "5.2.*", // v5.2.3
        "symfony/ux-chartjs": "^1.1", // v1.2.0
        "symfony/validator": "5.2.*", // v5.2.3
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.2.1
        "twig/intl-extra": "^3.2", // v3.2.1
        "twig/twig": "^2.12|^3.0" // v3.2.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.2.3
        "symfony/maker-bundle": "^1.27", // v1.30.0
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/stopwatch": "^5.2", // v5.2.3
        "symfony/var-dumper": "^5.2", // v5.2.3
        "symfony/web-profiler-bundle": "^5.2" // v5.2.3
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.13
        "@popperjs/core": "^2.9.1", // 2.9.1
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^5.0.0-beta2", // 5.0.0-beta2
        "core-js": "^3.0.0", // 3.8.3
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.1
        "react-dom": "^17.0.1", // 17.0.1
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "^2.0.1-phylor-6095f2a9", // 2.0.1-phylor-6095f2a9
        "stimulus-use": "^0.24.0-1", // 0.24.0-1
        "sweetalert2": "^10.13.0", // 10.14.0
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.0
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}
userVoice