Making a Configurable, Reusable 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

We can already reuse this new controller on any form where we want the user to confirm before submitting. That's awesome. But to truly unlock its potential, we need to make it configurable, giving us the ability to change the title, text, icon and confirm button text.

Fortunately, the values API makes this easy. At the top of our controller, add a static values = {} and... let's make a few things customizable. I'll use the same keys that SweetAlert uses. So we'll say title: String, text: String, icon: String and confirmButtonText: String. We could configure more... but that's enough for me.

... lines 1 - 3
export default class extends Controller {
static values = {
title: String,
text: String,
icon: String,
confirmButtonText: String,
}
... lines 11 - 29
}

Below, use these. Set title to this.titleValue or null. There's no built-in way to give a value a default... so it's common to use this "or" syntax. This means use titleValue if it's set and "truthy", else use null.

Let's do the others: this.textValue or null, this.iconValue or null and, down here this.confirmButtonTextValue or yes... because if you have a confirm button with no text... it looks silly.

... lines 1 - 11
onSubmit(event) {
... lines 13 - 14
Swal.fire({
title: this.titleValue || null,
text: this.textValue || null,
icon: this.iconValue || null,
... lines 19 - 21
confirmButtonText: this.confirmButtonTextValue || 'Yes',
}).then((result) => {
... lines 24 - 26
})
... line 28
}
... lines 30 - 31

I like this! Let's see how it looks if we don't pass any of these values. Refresh and... yup! It works... but probably we should configure those.

Head to the template - cart.html.twig - to pass them in. Do that by adding a 2nd argument to stimulus_controller(). Let's see, pass title set to "remove this item?", icon set to warning - there are five built-in icon types you can choose from - and confirmButtonText set to "yes, remove it".

... lines 1 - 2
{% block body %}
... lines 4 - 27
{% for item in cart.items %}
... lines 29 - 44
<form
... lines 46 - 50
{{ stimulus_controller('submit-confirm', {
title: 'Remove this item?',
icon: 'warning',
confirmButtonText: 'Yes, remove it'
}) }}
... line 56
>
... lines 58 - 62
</form>
... lines 64 - 69
{% endfor %}
... lines 71 - 89
{% endblock %}
... lines 91 - 92

Let's check it! Refresh and remove. That looks awesome! And more importantly, we can now properly re-use this on any form.

Submitting via AJAX

While we're here, I want to add one more option to our controller: the ability to submit the form - after confirmation - via Ajax instead of a normal form submit. Let me tell you... my ultimate goal. After confirming, I want to submit the form via Ajax then remove that row from the cart table without any full page refresh.

Quick side note about this. Our next tutorial in this series - which will be about Stimulus's sister technology "Turbo" - will show an even easier way to submit any form via Ajax. So definitely check that out.

But doing this with Stimulus will be a good exercise and will give us more control and flexibility over the process... which you sometimes need.

Setting up SweetAlert for the AJAX Submit

Ok: to support submitting via Ajax, we need to tweak our SweetAlert config. Add a showLoaderOnConfirm key set to true. Then add a preConfirm option set to an arrow function. This is going to replace the .then().

And... actually let's organize things a bit more: add a method down here called submitForm(). For now, just console.log('submitting form'). Then up in preConfirm, call this.submitForm().

... lines 1 - 3
export default class extends Controller {
... lines 5 - 11
onSubmit(event) {
... lines 13 - 14
Swal.fire({
... lines 16 - 21
confirmButtonText: this.confirmButtonTextValue || 'Yes',
showLoaderOnConfirm: true,
preConfirm: () => {
this.submitForm();
}
});
}
... line 29
submitForm() {
console.log('submitting form!');
}
}

This deserves some explanation. When you use the preConfirm option in SweetAlert, its callback will be executed after the user confirms the dialog. The big difference between this and what we had before - with .then() - is that this allows us to do something asynchronous - like an Ajax call - and the SweetAlert modal will stay open and show a loading icon until that Ajax call finishes.

Let's make sure we've got it hooked up. Refresh, and... yes! There's the log.

Submitting a Form via AJAX

Now let's actually submit that form via Ajax. Replace the console.log() with return fetch(). For the URL, this.element is a form... so we can use this.element.action. Pass an object as the second argument. This needs two things: the method - set to this.element.method - and the request body, which will be the form fields.

How do we get those? It's awesome! new URLSearchParams() - that's the object we used earlier - then new FormData() - that's another core JavaScript object... that even works in IE 11! - and pass this the form: this.element.

... lines 1 - 29
submitForm() {
return fetch(this.element.action, {
method: this.element.method,
body: new URLSearchParams(new FormData(this.element)),
});
}
... lines 36 - 37

That's a really nice way to submit a form via Ajax and include all of its fields. Oh, and notice the return. We're returning the Promise from fetch()... so that we can return that same Promise from preConfirm. When you return a Promise from preConfirm, instead of closing the modal immediately after clicking the "Yes" button, SweetAlert will wait for that Promise to finish. So, it will wait for our Ajax call to finish before closing.

... lines 1 - 11
onSubmit(event) {
... lines 13 - 14
Swal.fire({
... lines 16 - 23
preConfirm: () => {
return this.submitForm();
}
});
}
... lines 29 - 37

And we can now see this in action! Refresh and click remove. Watch the confirm button: it should turn into a loading icon while the Ajax call finishes. And... go!

Gorgeous! I think that worked! It didn't remove the row from the page - we still need to work on that - but if we refresh... it is gone.

Making the Ajax Form Submit Configurable

But I don't want this Ajax submit to always happen on all the forms where I use this confirm submit controller... because it requires extra work to, sort of, "reset" the page after the Ajax call finishes. So let's make this behavior configurable.

Over in the controller, up on values, add one more called submitAsync which will be a Boolean.

... lines 1 - 3
export default class extends Controller {
static values = {
... lines 6 - 9
submitAsync: Boolean,
}
... lines 12 - 42
}

Down in submitForm(), use that: if not this.submitAsyncValue, then this.element.submit() and return.

... lines 1 - 30
submitForm() {
if (!this.submitAsyncValue) {
this.element.submit();
return;
}
... lines 37 - 41
}
... lines 43 - 44

Let's make sure the Ajax call is gone. Actually... let me add a few more items to my cart... because it's getting kind of empty. Add the sofa in all three colors... then go back to the cart. Let's remove this one and... beautiful. It's back to the full page refresh.

Now let's reactivate the Ajax submit on just this form by passing in the submitAsync value. In the template, set submitAsync to true.

... 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 52 - 54
submitAsync: true,
}) }}
... line 57
>
... lines 59 - 63
</form>
... lines 65 - 70
{% endfor %}
... lines 72 - 90
{% endblock %}
... lines 92 - 93

At this point, we have a clean submit confirm controller that can be reused on any form. As a bonus, you can even tell it to submit the form via Ajax.

But when we submit via Ajax, we need to somehow remove the row that was just deleted. To do that, we're going to create a second controller around the entire cart area and make the two controllers communicate to each other. Teamwork? Yup, that's next.

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.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.12.1
        "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
    }
}