Chapters
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
The "defer" Attribute & Conditionally Activating Turbo
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Inspect element and go check out the head
tag. Notice that all of our script
elements live up here in the head
with a defer
attribute. That's on purpose. And this defer
attribute comes from our configuration: config/packages/webpack_encore.yaml
: script_attributes
, defer
Show Lines
|
// ... lines 1 - 6 |
# Set attributes that will be rendered on all script and link tags | |
script_attributes: | |
defer: true | |
# link_attributes: | |
Show Lines
|
// ... lines 11 - 31 |
The defer Attribute
The reason we placed our script
tags up in the head
element is... well, we learned why in the last chapter! By adding them here, they won't be re-executed on every Turbo visit.
But normally, adding script
tags to the head
is bad for performance. When your browser sees a script
tag, it freezes the page rendering while it downloads the file and executes it. But by adding defer
, the file is downloaded in the background and the page continues loading without waiting. Once the page finishes loading, then the JavaScript is executed. If you want to learn more about the defer
attribute, we have a blog post about it on symfony.com: https://symfony.com/blog/moving-script-inside-head-and-the-defer-attribute
Anyways, here's the big takeaway about using Turbo Drive and JavaScript: to get it to work reliably, all of your JavaScript needs to be written in Stimulus. But that does not mean that you need to completely rewrite it. If you have a big block of JavaScript that works on an element, you can copy that code into the connect()
method of a Stimulus controller, which is called each time a matching data-controller
element is found. Often, the only change you need to make is to remove any document.ready()
code and tweak your JavaScript to target this.element
.
And... if you can't or don't want to use Stimulus, you can also tweak your code so that it's executed on each "Turbo page load", like by wrapping that code in a Turbo event, that's fired on each visit instead of using jQuery's document.ready()
method. We'll talk about Turbo events later.
Completely Disabling Turbo
By the way, if you did need to disable Turbo for a specific link... or even for an entire section of the page, you can do that with a special data-turbo
attribute. For example, to completely disable Turbo drive on your entire site, head over to base.html.twig
. Find the body
tag and add data-turbo="false"
.
Show Lines
|
// ... lines 1 - 11 |
{% endblock %} | |
</head> | |
<body data-turbo="false"> | |
<div class="page-top"> | |
<header class="header px-2"> | |
Show Lines
|
// ... lines 17 - 84 |
Now, any link clicks or form submits inside of this element - which is everything - will not use Turbo drive. Check it out: refresh the page and click around. We are back to boring full page reloads. Boo.
To reenable Turbo Drive on a link or section, you can set the same attribute to true. For example, let's activate Drive for just the links up in the navbar
. Find that element... it's this ul
, and add data-turbo="true"
Show Lines
|
// ... lines 1 - 27 |
<ul class="navbar-nav" data-turbo="true"> | |
<li class="nav-item"> | |
<a class="nav-link" href="{{ path('app_cart') }}"> | |
Shopping Cart ({{ count_cart_items() }}) | |
</a> | |
</li> | |
Show Lines
|
// ... lines 35 - 84 |
Refresh again. When we click a category, it still triggers a full page reload. But if we click to go to the cart... that loaded with Drive! You can use this strategy to activate Turbo Drive on only some parts of your site that are ready.
Let's remove both of these to fully get Turbo Drive again.
Show Lines
|
// ... lines 1 - 12 |
</head> | |
<body> | |
<div class="page-top"> | |
<header class="header px-2"> | |
Show Lines
|
// ... lines 17 - 27 |
<ul class="navbar-nav"> | |
<li class="nav-item"> | |
<a class="nav-link" href="{{ path('app_cart') }}"> | |
Shopping Cart ({{ count_cart_items() }}) | |
</a> | |
</li> | |
Show Lines
|
// ... lines 35 - 84 |
Next: we've activated Turbo Drive and gotten the no-page-reload goodness with zero changes to our Symfony code! That's... amazing! But... there is one tiny change that we will need to make to any pages that have a form.
13 Comments
Hey Kevin B.!
That's a very simple, good question :D. The answer is... maybe? On a high level, yes, this should work. But, you'd need to try it to be sure. For example, in addition to attaching event listeners (all the normal document.ready() stuff), it's possible you might have some other JavaScript that truly shouldn't be run more than once (e.g. you call some initialization function on some 3rd party JS, which explodes if you do that twice on the same "page").
So this is conceptually a correct assumption, but my guess is that at least a few things would bite you (but if you find and squash those, it seems ok to me).
Cheers!
I've gone through and converted my inline javascript into stimulus controllers. My last one is I use FullCalendar, and I'm struggling to get it to work in a stimulus controller. The problem I'm having is that in my original inline javascript, I call routes from the twig template to load events, lots of them, I have 9 different routes I load into the calendar. I've looked into FOSJS so I can use the routes in the controller, but that isn't working for me. Do you have any suggestions? Another thing is that my route is passed with a variable as well. I'm using webpack encore, stimulus, symfony 5.3.7, everything is the latest versions. I tried this as well: url: "{{ path('event_orders', {id:jobid}, {'parameter':controller_value}) }}", but I don't get anything in the console and the event doesn't load. Any help is much appreciated, thank you for your hard work.
Hi Brandon!
What I usually do in such cases is simply to print out these routes values of the controller using twig. This way you don't need any client-side solution and the result is portable and simply works. For example, in twig, you can initialize your controller like this:
<div
class="my-controller-div"
{{ stimulus_controller('my-controller', { myRoute: path('path-to-my-route', { id: product.id }) }) }}
>
More HTML here
</div>
This way in your controller, you can access all these route through the Values API:
export default class extends Controller {
static values = {
myRoute: String,
};
.
.
.
}
Hope this helps!
Quick question again, I see how this works when adding myRoute to the stimulus_controller call, how would I add this to something else, like:
In short, how do I send the data-url value to delete_controller#remove function? The value comes from a database for loop in
a twig template. Any help is much appreciated, thank you!
The following is in A tag
data-action="click->delete#remove"
data-url="{{ path('labor_board_delete', {id:worker.id}) }}"
class="delete"
This didn't post before, sorry
Playing with this more, I guess my real question is it bad practice to call {{ stimulus_controller('my-controller', { myRoute: path(...)} in a for loop on a twig template?
Hey @Brandon
I think it depends on your situation. Do you need to instantiate a Stimulus controller per each item in your loop? Or, a single Stimulus instance it's enough but you just need to get all of the routes?
A single instance is all I need, I'm just unclear on how to get all the routes to the controller in the loop
Hi Brandon! If you have a case where the values API of stimulus isn't good enough for your needs, and you have a data-url attribute in the HTML element (in the case you posted, the element would also be the event's target), you can retrieve the URL as follows:
remove(event) {
const url = event.target.dataset.url;
.
.
.
}
I'm not sure if this is what you mena in your question!
Matias, thank you for the help. I love PHP but I do struggle with JavaScript. My problem is that I'm using drag and drop, and when I drag something with a certain class, like "warning", it does an Ajax call to a certain URL, and if the class contains "success", I go to a different URL. It all works, but not consistently. When I drag something with a class of "warning" in my console it says I dragged something with "success", but not every time. And when I inspect the element, the class I'm dragging doesn't match what my console.log says it is. I've spent days trying to figure this out, and my previous questions were all ideas I've been trying to solve it. Thank you for your help, it has gotten me closer and other parts of my code are better for it.
No problem! I hope you get it working 100%!
Perfect, I'm able to console.log myRoute and it works! Thank you so much for your help.
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// 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.21.6
"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
}
}
What JavaScript libraries does this tutorial use?
// 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
}
}
Hello, I wanted to clarify something. If I want my app to use turbo (quickly) and I have a huge, ugly jquery document ready function (I don't want to immediately split this out into stimulus controllers), I can just move it into a turbo:load event and be good to go?