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 SubscribeLet'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!
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.
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
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 asubmit
event... that triggers oursubmit
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.
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!
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 !
yeah, it's not a clean approach but you could create your own Twig function to hide and reuse the replacing functionality
Cheers!
// 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
}
}
// 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
}
}
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