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 SubscribeHead back to the Turbo docs, specifically to Reference and then Events. We saw this list of events earlier. Now we're going to hook into a new one: turbo:before-render
.
Here it is. This event triggers before Turbo renders a page, but not counting the initial page load. In other words, it triggers when Turbo is specifically responsible for rendering the page. We can use this to help our third party weather widget get working right before the page renders.
Head over to assets/turbo/turbo-helper.js
and, up here in the constructor
... say document.addEventListener()
to listen to turbo:before-render
. Pass this an arrow function and then log "before render" so we can see exactly when this does and doesn't execute.
... lines 1 - 3 | |
constructor() { | |
document.addEventListener('turbo:before-cache', () => { | |
this.closeModal(); | |
this.closeSweetalert(); | |
}); | |
document.addEventListener('turbo:before-render', () => { | |
console.log('before render!'); | |
}); | |
} | |
... lines 14 - 41 |
Cool. Let's test it!
Find your browser, refresh, and open the console. Okay. So nothing on initial page load. But then, when we click to another page, there it is! Click to another page... there's a second one. Click to the homepage, a third one. Awesome.
Now, clear out the console... and go back to a page we went to a second ago. It logs twice! This is an important detail about this event. It fired twice because first the preview was rendered and then the final page was rendered. Just keep that fact in mind.
So here's the plan: right before the page is rendered, so inside of our new listener, we're going to find and remove this weatherwidget-io-js
script tag. Then, with any luck, when the new page is loaded, the JavaScript from our base template will execute, it will re-add that script tag and everything will work!
Let's check it! Replace the log with document.querySelector()
and look for #weatherwidget-io-js
. Then say, .remove()
. You can also code defensively to make sure the element exists first before trying to call remove()
... not a bad idea.
... lines 1 - 3 | |
constructor() { | |
document.addEventListener('turbo:before-cache', () => { | |
this.closeModal(); | |
this.closeSweetalert(); | |
}); | |
document.addEventListener('turbo:before-render', () => { | |
document.querySelector('#weatherwidget-io-js').remove(); | |
}); | |
} | |
... lines 14 - 41 |
Ok: refresh. It works and... navigate to a different page. Yea! It still works! If you look inside the head
element, it accomplishes this without duplicating the script
tag.
I like this solution. But if you're willing to do some digging, there might be an alternate solution.
Copy the widget.min.js
URL and open it in your browser. It's minified... so pretty unreadable. Copy the source, close it, spin over to your editor and create a new file anywhere, like pizza.js
... we're not going to actually use this. Paste the code, select it, then go back up to Code -> Reformat Code so we can at least, kind of read it.
It's still not super clear, but... let's see. Ah! There's a function called __weatherwidget_init
... and it looks like this might be the key to re-initializing the weather widget! In other words, instead of removing and re-adding the script
tag on each render, we might be able to just... call this function!
Let's do some experimenting! Start by changing the event from turbo:before-render
to turbo:render
... that's another new event. Why are we switching to it? In order for the __weatherwidget_init
function to work, the new weatherwidget-io
anchor tag needs to actually live on the page.
But turbo:before-render
is triggered too early: it's triggered before the new body is on the page. Fortunately, turbo:render
is called after it's on the page. This means that, inside of the callback, we know that the new body will be on the page. And so, we can call that __weatherwidget_init
function. Let me steal that name from the other file... and paste it here.
... lines 1 - 3 | |
constructor() { | |
document.addEventListener('turbo:before-cache', () => { | |
this.closeModal(); | |
this.closeSweetalert(); | |
}); | |
document.addEventListener('turbo:render', () => { | |
__weatherwidget_init(); | |
}); | |
} | |
... lines 14 - 41 |
Testing time! Refresh! The first page works: no surprise. And when we go to a second page... yes! It still works! No matter how many pages we go to, it keeps working. I like this solution better, though, I also realize that we're sort of using an "internal" function from that widget script... and it's possible they could change their JavaScript some time in the future.
Now that we have this working, let's refactor this logic into a method for clarity. Copy the __weatherwidget_init
function, go to the bottom of the class and create a new method, how about initializeWeatherWidget
. Paste, then call that from up here in our listener: this.initializeWeatherWidget()
.
... lines 1 - 3 | |
constructor() { | |
document.addEventListener('turbo:before-cache', () => { | |
this.closeModal(); | |
this.closeSweetalert(); | |
}); | |
document.addEventListener('turbo:render', () => { | |
this.initializeWeatherWidget(); | |
}); | |
} | |
... lines 14 - 38 | |
initializeWeatherWidget() { | |
__weatherwidget_init(); | |
} | |
... lines 42 - 45 |
By the way, there is a third way to solve this problem, and we'll talk about it later. It's especially appropriate if you need to load an external widget - like our weather widget - but that widget might be loaded onto the page at any time, even via a custom, non-Turbo Drive Ajax call. This solution basically involves running the same code that we have here, but leveraging a Stimulus controller.
Before we move on, we do need to talk about one last type of external JavaScript: analytics code. As an example, here's what Google analytics code looks like: this is what you're supposed to paste into the head
tag of your page.
It turns out that the key line that triggers the visit is this last one: gtag('config')
. If we pasted all of this onto our site, guess what would happen? It would register the first visit... then the code would never execute again, no matter how many pages the user visited. That's not great. Fortunately, single page applications - like those written in Vue or React - have the same problem.... and you can often find docs that talk about how to integrate with those.
In this case, the solution would be to paste all this code - except for the gtag('config')
line - into your head
like normal. For this last line, we need to execute it on initial page load and then every Turbo "visit" after.
Let me open a GitHub issue that talks about this with a really nice solution. As you can see here, henrik
is using a turbo:load
event. That's yet another event that we haven't talked about yet. turbo:load
is nice because it's executed on initial page load and one time for every visit: it avoids the "double dispatch" that happens with the turbo:before-render
and turbo:render
events when you visit a page that shows a preview. In other words, turbo:load
is triggered exactly when you would want your analytics code to trigger a visit.
Inside the callback, henrik
calls gtag('config')
to trigger that visit. This googleAnalyticsIDForScript
thing is just their way of referencing whatever your Google Analytics ID is. The one special thing that you need to do with this function is pass a little bit of extra data to make sure analytics knows what the actual URL is that it should use.
Next: we already know that, with Turbo Drive, we download each CSS and JavaScript file just one time. Then, as we navigate around, if Turbo sees a CSS or JS file in the new page's head
tag that already exists on the current page, it ignores it.
But what happens if we deploy a new version of our site and the content of these files has changed? How can we force the user to download the newest version of our assets? That's an important question.... and one where the answer is refreshingly simple.
Hey Nick F. !
Haha, that's a good question! Technically speaking (to borrow from your definition)
turbo:before-visit does NOT fire on the initial page load, but fires once and only once *before* each subsequent page is visited.
So, that technical definition makes sense, but your question is its "use-case". I'm not sure. However, I DO know that you can *cancel* a visit from this event. So, in theory, someone could click, initiate a visit, and you intercept that and stop it. I did find one example of a user doing this to prevent the visit so that they could load the page from some sort of cache... but I can't find many examples of this being used in the real-world.
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.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.18.5
"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
}
}
// package.json
{
"devDependencies": {
"@babel/preset-react": "^7.0.0", // 7.13.13
"@fortawesome/fontawesome-free": "^5.15.3", // 5.15.3
"@hotwired/turbo": "^7.0.0-beta.5", // 1.2.6
"@popperjs/core": "^2.9.1", // 2.9.2
"@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
"@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/Resources/assets", // 0.1.0
"@symfony/ux-turbo-mercure": "file:vendor/symfony/ux-turbo-mercure/Resources/assets", // 0.1.0
"@symfony/webpack-encore": "^1.0.0", // 1.3.0
"bootstrap": "^5.0.0-beta2", // 5.0.1
"chart.js": "^2.9.4",
"core-js": "^3.0.0", // 3.13.0
"jquery": "^3.6.0", // 3.6.0
"react": "^17.0.1", // 17.0.2
"react-dom": "^17.0.1", // 17.0.2
"regenerator-runtime": "^0.13.2", // 0.13.7
"stimulus": "^2.0.0", // 2.0.0
"stimulus-autocomplete": "https://github.com/weaverryan/stimulus-autocomplete#toggle-event-always-dist", // 2.0.0
"stimulus-use": "^0.24.0-1", // 0.24.0-2
"sweetalert2": "^11.0.8", // 11.0.12
"webpack-bundle-analyzer": "^4.4.0", // 4.4.2
"webpack-notifier": "^1.6.0" // 1.13.0
}
}
So in summary:
turbo:load is actually the only event that fires on the initial load, as well as once on every page visit.
turbo:visit does NOT fire on the initial page load, but fires once and only once on each subsequent page visited.
turbo:before-render fires ATLEAST once on every page BEFORE the body content is loaded/refreshed; twice if there was a preview shown.
turbo:render fires ATLEAST once on every page, AFTER body content is loaded/refreshed; twice if there was a preview shown.
What is the use case for turbo:before-visit?