Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The "defer" Attribute & Conditionally Activating Turbo

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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

... lines 1 - 6
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
# link_attributes:
... 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".

... lines 1 - 11
{% endblock %}
</head>
<body data-turbo="false">
<div class="page-top">
<header class="header px-2">
... 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"

... 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>
... 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.

... lines 1 - 12
</head>
<body>
<div class="page-top">
<header class="header px-2">
... 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>
... 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.

Leave a comment!

13
Login or Register to join the conversation
Kevin B. Avatar
Kevin B. Avatar Kevin B. | posted 7 months ago

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?

Reply

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!

1 Reply
Brandon Avatar
Brandon Avatar Brandon | posted 1 year ago

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.

Reply

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!

Reply
Brandon Avatar

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!

Reply
Brandon Avatar

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

Reply
Brandon Avatar

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?

Reply

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?

Reply
Brandon Avatar

A single instance is all I need, I'm just unclear on how to get all the routes to the controller in the loop

Reply

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!

Reply
Brandon Avatar

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.

Reply

No problem! I hope you get it working 100%!

Reply
Brandon Avatar

Perfect, I'm able to console.log myRoute and it works! Thank you so much for your help.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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