Organizing our Turbo Events Code

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

To get Turbo Drive to work super nicely, we're going to need to hook into a few events, like turbo:before-cache. Before we're done, we'll listen into even more of these to help us properly load JavaScript widgets, add transitions, and do more craziness when we talk about Turbo Frames.

Isolating the Turbo Logic

So instead of putting all that logic right here in app.js, let's organize a bit. There's no right or wrong way to do this, but let's create a class that holds all of the special Turbo logic. In the assets/ directory, add a sub-directory called turbo/ and, inside, a new file: turbo-helper.js. Start with const TurboHelper = class {} with a constructor() {} inside.

Now, head back to app.js, copy all of this code, and paste! When we did that, PhpStorm helpfully added the import { Modal } for us. At the bottom of this file, export default new TurboHelper().

import { Modal } from 'bootstrap';
const TurboHelper = class {
constructor() {
document.addEventListener('turbo:before-cache', () => {
if (document.body.classList.contains('modal-open')) {
const modalEl = document.querySelector('.modal');
const modal = Modal.getInstance(modalEl);
modalEl.classList.remove('fade');
modal._backdrop._config.isAnimated = false;
modal.hide();
modal.dispose();
}
// internal way to see if sweetalert2 has been imported yet
if (__webpack_modules__[require.resolveWeak('sweetalert2')]) {
// because we know it's been imported, this will run synchronously
import(/* webpackMode: 'weak' */'sweetalert2').then((Swal) => {
if (Swal.default.isVisible()) {
Swal.default.getPopup().style.animationDuration = '0ms'
Swal.default.close();
}
})
}
});
}
}
export default new TurboHelper();

This is kind of cool: it instantiates a new instance of our object and exports it. It won't really matter for us... but thanks to this, each time we import this module, we will get the same one instance of this object.

In app.js, delete all the original code and then import './turbo/turbo-helper'. We don't need to set that to a variable... and just by importing it, the object will be instantiated and the listeners will be registered. So... this should be enough to get things working!

15 lines assets/app.js
... lines 1 - 10
// start the Stimulus application
import './bootstrap';
import './turbo/turbo-helper';

Let's try! Refresh, click to remove an item, go back and go forward. Yep! All good.

Organizing the Class

Now that we have a class, we can organize a bit more. Copy the modal code, remove it, create a new method below called closeModal() and paste. Then, back up inside the turbo:before-cache callback, say this.closeModal().

Repeat this for Sweetalert: copy all of the Sweetalert code, create a new method called closeSweetalert(), paste... and... then back in the callback, use it: this.closeSweetalert().

import { Modal } from 'bootstrap';
const TurboHelper = class {
constructor() {
document.addEventListener('turbo:before-cache', () => {
this.closeModal();
this.closeSweetalert();
});
}
closeModal() {
if (document.body.classList.contains('modal-open')) {
const modalEl = document.querySelector('.modal');
const modal = Modal.getInstance(modalEl);
modalEl.classList.remove('fade');
modal._backdrop._config.isAnimated = false;
modal.hide();
modal.dispose();
}
}
closeSweetalert() {
// internal way to see if sweetalert2 has been imported yet
if (__webpack_modules__[require.resolveWeak('sweetalert2')]) {
// because we know it's been imported, this will run synchronously
import(/* webpackMode: 'weak' */'sweetalert2').then((Swal) => {
if (Swal.default.isVisible()) {
Swal.default.getPopup().style.animationDuration = '0ms'
Swal.default.close();
}
})
}
}
}
export default new TurboHelper();

That looks better! Let's... make sure we didn't mess anything up. Do the same dance as before: refresh, click remove, go back and go forward. All good!

Next: let's learn what types of things can go wrong when including third-party hosted JavaScript, like a JavaScript widget or analytics code. This type of JavaScript is usually supposed to be included in the body of the page... and often it expects full page refreshes.

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/ux-turbo-mercure": "^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
    }
}