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 SubscribeIn a perfect world, all your JavaScript would be written in Stimulus and you would have zero script
elements in your body
tag. With that ideal setup, your JavaScript would always work - regardless of how or when new HTML was loaded - and it would only be parsed and executed one time, on initial page load.
But what about externally-hosted JavaScript? I'm talking about a third party service that you sign up for... then you're supposed to copy some JavaScript from their site, paste it onto your site... and suddenly you get a "feedback" button or a "share on Twitter" button... or maybe it's analytics JavaScript. These bits of JavaScript will definitely not be written in Stimulus and, often, funny things start to happen when you use them. Not, like "funny haha", more funny weird...
Let's see an example. Let's integrate a third-party weather widget onto our site. Head over to weatherwidget.io, which, as its name suggests, allows us to embed a handy weather widget onto our site.
Click this "get code" button. So this is pretty common: you sign up for some service and then they give you some JavaScript that you're supposed to paste onto your site.
Let's do it: copy this... then go open templates/base.html.twig
. Head to the bottom and paste this in the footer: right before the closing body
tag... though you could put this anywhere.
... lines 1 - 75 | |
{% block body %}{% endblock %} | |
<div | |
class="footer mb-0" | |
{{ stimulus_controller('made-with-love') }} | |
> | |
</div> | |
<a class="weatherwidget-io" href="https://forecast7.com/en/40d71n74d01/new-york/" data-label_1="NEW YORK" data-label_2="WEATHER" data-theme="original" >NEW YORK WEATHER</a> | |
<script> | |
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src='https://weatherwidget.io/js/widget.min.js';fjs.parentNode.insertBefore(js,fjs);}}(document,'script','weatherwidget-io-js'); | |
</script> | |
</body> | |
</html> |
Cool: this gives us an a
tag... which just says "New York weather". Then, my guess is that this JavaScript will execute and transform that a
tag into the cool weather widget that you see down here.
Let's find out! Do a whole page refresh, scroll all the way down and yes! We have a weather widget! Now, navigate to another page and... it's broken! It's just the original anchor tag. Where did our cool little widget go?
The JavaScript code that we pasted is pretty impossible to read. To help, select it and then go to Code -> Reformat code. There we go! It's still a little hard to read, but it's doable.
... lines 1 - 83 | |
<a class="weatherwidget-io" href="https://forecast7.com/en/40d71n74d01/new-york/" data-label_1="NEW YORK" data-label_2="WEATHER" data-theme="original" >NEW YORK WEATHER</a> | |
<script> | |
!function (d, s, id) { | |
var js, fjs = d.getElementsByTagName(s)[0]; | |
if (!d.getElementById(id)) { | |
js = d.createElement(s); | |
js.id = id; | |
js.src = 'https://weatherwidget.io/js/widget.min.js'; | |
fjs.parentNode.insertBefore(js, fjs); | |
} | |
}(document, 'script', 'weatherwidget-io-js'); | |
</script> | |
... lines 96 - 98 |
This is a function that calls itself and passes in these three arguments. Basically, when this JavaScript is executed, it adds a new script
tag to the head
element of our page that points to this widget.min.js
script on their site. But this function is smart: it gives the script
tag an id set to weatherwidget-io-js
. And before it adds the script
tag, it checks to see if it's already on the page. If the script
tag does already exist, it avoids adding it twice.
Back over at our browser, find and expand the head
tag. Yup! There's the script
tag with id="weatherwidget-io-js
that points to widget.min.js
.
So here's what's going wrong in our case. When the page first loads, like right now, this JavaScript function executes and the new widget.min.js
script tag is added to our page. Our browser downloads that file and then, my guess is that, when that JavaScript executes, it looks for elements with a weatherwidget-io
class on it and transforms them into the fancy weather widget.
Inspect element on this. Yup! There's the anchor tag... but now with a big iframe
inside.
But then, when we navigate to another page, the entire body
tag is replaced by a new body
tag. The weather widget that lives inside the original anchor
tag is now gone from the page, replaced by a new anchor tag that's just the original boring one that says "New York weather".
However, Turbo does see the script
tag that's inside of the new body - the script tag that we have down at the bottom of base.html.twig
- and it does re-execute these lines. But this time, since the script with id weatherwidget-io-js
already exists up here in the head
tag, it does not re-add it to our page. And so, no JavaScript ever runs that re-initializes the widget into our new anchor tag.
Okay, so now that we understand what's going on, shouldn't we just, you know, tweak the JavaScript so that it always inserts the script
tag? Let's try it. I'll cheat and temporarily add || true
to the if statement so that it always executes and adds that element.
All right. Refresh. On page one, the weather widget works. Click over to the cart and... yea! The weather widget still works! Problem solved! And don't worry, the script
tag isn't downloaded multiple times: your browser is smart enough to pull it from cache after it downloads it the first time.
But... this might not be the best solution for two reasons. Look at the head
element of our page. Woh! We have two script tag!. And each time we navigate, we would get yet another one.
That... might be ok? But it seems a bit crazy: eventually a user might have 50 identical script
elements on their page.
And actually, that's precisely how some external JavaScript works. Some external JavaScript snippets do not have this if statement here. And so, one of the problems is that it does add more and more and more script tags when you using Turbo.
The second problem is that... whether or not executing this script file over and over again is a good idea... sort of depends on what that script
tag does! If it simply reinitialize the weather widget, cool! That sounds safe. But if it, for example, adds an event listener to the document each time it's executed, then each time we load that script tag, we're going to add a second, third, fourth, or fifth listener. Then, suddenly when you, for example, click the page, that JavaScript widget's listener will execute 5 times and... do whatever it normally does way more times than normal.
My point is: you need to be careful with third-party JavaScript. Let's put back the if statement the way we found it.
So in this case, re-executing the widget.min.js
script tag after each visit is probably okay: it does seem to simply reinitialize the weather widget on this element. But I would love to do that without duplicating the script
tag and ending up with 50 of them in my head
. How can we do that? By removing the previous script
tag right before the page renders. And how can we do that? Via a new event listener. Let's talk about that next and discuss the proper way to handle analytics code so that you don't under-count or over count your visits.
Hey @Strahil-R!
No, no personal experience - sorry. I assume the problem is related to re-initializing their "widget" after each Turbo navigation? Or is it something else? Assuming that this widget adds some new markup to the bottom of your page that you want to keep, you could, in theory (though I've never tried this), via JavaScript add a data-turbo-permanent
attribute to some element that surrounds this widget. Then, when you navigate, Turbo will "not touch" this element.
Cheers!
Hey @weaverryan,
this is exactly the problem. I loads correctly on first page load, but fails on subsequent turbo visits. I will try out your proposal, thanks for that!
My hypotheses at the moment (after downloading the source code of the widget and trying to make sense of it) is that it does something on window.load event. And since this event is not fired on turbo visits the widget is not initialised properly.
Hey Strahil-R!
I will try out your proposal, thanks for that!
Let me know how it goes!
My hypotheses at the moment (after downloading the source code of the widget and trying to make sense of it) is that it does something on window.load event. And since this event is not fired on turbo visits the widget is not initialised properly.
This is an excellent theory. Unfortunately, it's still quite common for external JavaScript to only work on "real page loads". That should improve... because this also wouldn't work well with SPA's.
Cheers!
I tried to come up with a solution on my own and the first one that came to my mind was to insert the widget script tag at the end at the body tag, this way it would be reloaded each time a new page is loaded,
do you consider this solution valid?
Thanks!
Hey @Maxime!
I would say... yes! Probably... maybe! :D
That is *likely* a valid solution. But here are a few things to think about:
A) Putting the script tag in body WILL cause the JavaScript inside of it to be re-executed on each visit. However, some external JavaScript still does not "expect" to be executed multiple times during the same true "page load". That is actually the case with the weather widget we add. Even if you put this in your body tag, without any other modifications, it is written in such a way that it only *truly* activates the weather widget one time per *true* page load... no matter how many times you execute that script.
B) There is a (probably) minor performance penalty for putting scripts in the body tag, which is why we are - in general - trying to avoid it. By putting the script in your head... and then doing something fancy to "reactivate" that widget on each visit, you are making it so that the original script only needs to be parsed once. If you put it in the body, it will be parsed and re-executed on ever visit. But for one or a few external JavaScript files, the performance penalty is probably not a big deal, but it's a nice thing to keep in mind.
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
}
}
Hi @weaverryan!
any experience with integrating zopim/zendesk chat widget in a project using turbo? I have been trying different approached, but no luck so far :(