Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Symfony UX: Turbo

6:19:46

What you'll be learning

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

Think you need to build an SPA (single page app) in React or Vue to make your site feel like an SPA? Think again!

In part 1 of this series - Symfony UX: Stimulus - we talked about a library that allows you to attach JavaScript functionality to HTML elements - even if that HTML is loaded via AJAX.

In this course, we'll talk about Turbo: a JavaScript library that instantly makes your site feel like an SPA. How? By turning every link and form into an AJAX call... without you needing to make any changes to your app (well, almost no changes):

  • Understanding and enabling Turbo Drive!
  • Reloading the page when assets change
  • Form updates: returning a 4xx status code
  • AJAX loading parts of your page with Turbo Frames
  • Symfony'x ux-turbo package!
  • Making links/form submits "stay" inside a frame
  • Targeting the main page from a frame
  • Turbo streams: updating multiple parts of your page from the server!

The end result? A "traditional" site (one that returns HTML) that absolutely flies!


Your Guides

Ryan Weaver

Buy Access
Login or register to track your progress!

Join the Conversation?

27
Login or Register to join the conversation
Ueli B. Avatar
Ueli B. Avatar Ueli B. | posted 1 year ago

Don't feel pressured by me either but i can hardly wait!

5 Reply

We're working at max speed to deliver this course, just be a bit more patience :)

Cheers!

1 Reply

Hi! I see that Turbo feature was merged into the main symfony-ux branch recently. When can we expect the first chapters on this course? I'm not trying to put any pressure on you guys, I know that you have a lot of things to do and I really appreciate your hard work on making these awesome courses! I'm just very excited about turbo and cannot wait to see your take on using it!

3 Reply

Hey DjTapke,

Good catch! It's still top-secret for now, (so don't tell anyone! :) ), but we're going to start working on it in late April / early May. It's very rough estimation, so things may change anytime, but I don't think we will have it earlier than in May :) I would like to suggest you to subscribe to this course on the course intro page: https://symfonycasts.com/sc... - we will notify you when we will start releasing it.

P.S. We're very excited about this Turbo feature as well in SymfonyCasts! ;)

Cheers!

2 Reply

What is the sound of one developer waiting patiently?

1 Reply

the sound of clicking here and there? :p

1 Reply
Tomato Avatar

Hello!
I have a problem to run the script, can not run yarn watch. Please, see below error from console. Thanks for your help.
yarn run v1.22.19<br />$ encore dev --watch<br />/bin/sh: 1: encore: not found<br />error Command failed with exit code 127.<br />info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Reply

Hey Tomato,

Double-check the scripts section of your package.json file, you should have something like this

    "scripts": {
        "dev-server": "encore dev-server",
        "dev": "encore dev",
        "watch": "encore dev --watch",
        "build": "encore production --progress"
    }

then, try running the command this way yarn watch

I hope it helps. Cheers!

Reply
Default user avatar
Default user avatar Julien Quintiao | posted 5 months ago

Hi !

Awesome course as usual :)
I have a question, i use a lot some admin librairies like Sonata Admin or more recently Easy Admin.

I just saw that EasyAdmin disabled UX-turbo because of some conflicts with their body javascripts loads and so on ...
https://github.com/EasyCorp...

"Refactoring everything to be turbo-compatible would be titanic effort with little benefit"

Would you have some suggestions to still use it with those librairies ?

Reply

Yo Julien Quintiao!

That's a really great question. The short answer is: use those libraries... but disable turbo for those sections :). For Turbo to work (as you know), your JavaScript needs to be written "correctly". For something like EasyAdmin, there is a lot of JavaScript that you don't have control over. And so, there's nothing you can do. But I don't think it's a huge deal: use Turbo where you can, and skip it where you can't.

I'd love to see EasyAdmin refactor their JavaScript to use Stimulus. Their JavaScript isn't "incorrect" currently, but it would be awesome to see it done in Stimulus. But, it *would* be a big job.

Cheers!

Reply
Dave B. Avatar

Hi

Thanks for all the great content.

Was just wondering if there is some way to keep the existing Turbo frame content instead of replacing it with a temporary loading icon etc until new frame content is rendered?

For example

My Mercure Turbo Stream returns a frame so that I can render the content specific for each user e.g.


<turbo-stream action="update" target="{{ my_frame_id }}">
<template>
<turbo-frame id="{{ my_frame_id }}" src="{{ path_to_get_new_content }}">

<div style="height: 1296px;"></div>

</turbo-frame>
</template>
</turbo-stream>

I have chucked an empty div so that the page does not scroll up when page reloads.

Should I just re-render the view in place of the div or is there a way to keep the existing html until the new html replaces it?

Thanks
Dave

Reply

Hey Dave B.!

Hmm, that's an interesting question! My first instinct was to say "just return the turbo-frame WITH the final content (instead of a lazy turbo-frame with src="") from the turbo-stream". However, I think see why you're doing it this way: this update will go to MANY users... and then each user's browser will then make the "lazy turbo frame" Ajax request to get THEIR personal data. Is that right? If so, it's a pretty clever way to update different content for many different users at the same time :).

Anyways, back to your problem. I also can't think of a "slick" way to fix this, I don't believe there is any mechanism inside of turbo to allow this. So, you would need to do something tricky. For example, you could do something like:


<turbo-stream action="before" target="{{ my_frame_id }}">
<template>
<turbo-frame id="tmp_frame_update" src="{{ path_to_special_controller }}"></turbo-frame>
</template>
</turbo-stream>

I've changed the action to before. Instead of replacing the frame, I am adding a NEW frame right before it, that points to some OTHER controller (not the controller that returns the "{{ my_frame_id }}" content. Anyways, in that controller, you would *return* (not dispatch to Mercure, just return a Turbo stream response, I believe that would work... and not cause a frame error, but we'll see) another TurboStream that looks something like:


<turbo-stream action="update" target="{{ my_frame_id }}">
<template>
<turbo-frame id="{{ my_frame_id }}">
THE REAL CONTENT HERE
</turbo-frame>
</template>
</turbo-stream>
<turbo-stream action="remove" target="tmp_frame_update"></turbo-stream>

Does that makes sense? It's kinda wacky... and I'm not entirely user that returning the TurboStream from the turbo frame Ajax call will be allowed... but we'll see. Another, similar idea, would be to do this exact same thing, except that instead of adding a turbo-frame id="tmp_frame_update" from the original turbo-stream, you could add a div element with a stimulus_controller() on it. That controller would then make the ajax call to the endpoint which would return the second turbo stream. You could then take that response and process it through the Turbo stream system (we show how to do that here https://symfonycasts.com/sc....

Let me know if any of this helps or makes sense!

Cheers!

Reply
Dave B. Avatar

Hi Ryan

Thanks. Yes I wanted to use new Update() or Entity Broadcast to render user specific views.

Returning the turbo frame with the src to get this content works well e.g.


// _list.stream.html.twig
<turbo-stream action="update" target="{{ turbo_frame_id }}">
<template>
<turbo-frame id="{{ turbo_frame_id }}" src="{{ path('_app_product_crud_list_page', {
stream: true,
template: 'product_admin/crud/_list_content.html.twig'
}) }}">
Loading...
</div>
</turbo-frame>
</template>
</turbo-stream>

However a side effect was that it caused the Browsers Window to scroll up because the Users turbo-frame content momentarily gets replaced with the Loading html which takes up less height than the view content.

Original Temp Solution:

I first just approximated the size of the current turbo frame and in my '_list.stream.html.twig' replaced the loading html with a fixed height div e.g.


// _list.stream.html.twig
<turbo-stream action="update" target="{{ turbo_frame_id }}">
<template>
<turbo-frame .....="">
<div style="height: 1296px;">
</div>
</turbo-frame>
</template>
</turbo-stream>

New Solution (seems to work so far!) :

Using the turbo-helper file add another event listener, watch for the before-stream-render response and replace the 'Loading' html with the current html showing in the view :


const TurboHelper = class {
constructor() {

...otherListeners

document.addEventListener('turbo:before-stream-render', (e) => {
const frame = e.target.getElementsByTagName( 'template' )[0].content.children[0]
frame.innerHTML = document.getElementById(frame.id).innerHTML
});
}

This seems to work and means I can use Entity Broadcast and new Update() to update User specific views without the page jumping around.

Im sure there is a cleaner way to write this btw.

Cheers

Reply
Dave B. Avatar

BTW to keep normal streams working as they should you need to add a check in the new event listener e.g.


document.addEventListener('turbo:before-stream-render', (e) => {
const frame = e.target.getElementsByTagName( 'template' )[0].content.children[0]
if(frame.tagName.toLowerCase() === 'turbo-frame' && frame.hasAttribute('src')) {
frame.innerHTML = document.getElementById(frame.id).innerHTML
}
});

Reply

Hey Dave B.!

That's an awesome solution! I wasn't thinking about event listeners at all - I love this - well done, and thanks for sharing.

Cheers!

Reply
Ruslan Avatar

Hi, one stupid question from me :)
By your opinion for classic symfony app MPA (full state, with Twig) , What is the best choice Vue, React, Stimulus or some thing else?
I see many tehnologies on SymfonyCasts, but I haven't much time to study all these frameworks/libs.
Thank you.

Reply

Hey Ruslan!

That's a pretty fair question :). My choice would be a multi-page app with Twig, but with Turbo and Stimulus immediately so that you can get the "single page app" feel. However, if what you're building is *quite* dynamic (it's more of an "app" on the web than a "web site"), you could make a strong argument for Vue or React. React is used by more people, but, Vue is quite popular and loved by its user base.

If you have any other questions or doubts, let me know :). On SymfonyCasts, we use Stimulus (and are in the process of adding Turbo). We also use React few a few super dynamic "widgets" on our site - like the coding challenges that are on some tutorials.

Cheers!

1 Reply
Ruslan Avatar

Thank you!

Reply
Nick-F Avatar

Before I dig into this, could someone explain to me the practical benefits of turning everything into ajax?

Reply

Yo Nick F.!

That's an excellent question :). It basically makes your site navigate more quickly & gives it more of a "single page app" feel. At first, you are probably wondering (as I did) something like:

> How? If Turbo changes clicks/submits to Ajax calls... but those requests STILL return the same full HTML page, how is that "faster"?

The answer (or at least the biggest reason, from my research) is that your JavaScript & CSS do not need to be re-parsed. Consider the "flow" for the 2 situations:

1) Traditional full page refresh:

A) the page is "downloaded"
B) all of your JavaScript is parsed & executed
C) all of your CSS is parsed
... then this fully repeats on every click. Your JavaScript and CSS may be cached (and that helps!) but they are always re-parsed.

2) Turbo-powered Ajax (imagine that they "first" page was already loaded, which includes the steps above and now you click a link)

A) the page is "downloaded" (same as above)
B) The HTML is put onto the page

That's it. Instead of your JavaScript being reparsed and reloaded on every navigation, it becomes a, sort of, "persistent" engine: it's parsed just once, then it just runs forever.

This, apparently, makes a big difference - because your site *does* feel a *lot* faster (there may also be some psychological effect to not seeing your browser "loading", but I'm just guessing).

If you have any other questions, let me know :).

Cheers!

1 Reply
It O. Avatar

hey guys, can't still waiting... 😬

Reply

I know! We're working at max speed to release this tutorial, please wait just a bit more

Reply
Michael S. Avatar
Michael S. Avatar Michael S. | posted 1 year ago

With the sudden departure of the 2 maintainers of Turbo and Stimulus which leaves the projects pretty much dead for now, will this course still be released?

Reply

Hey Michael S.!

Absolutely! I was very sad to see the 2 maintainers leave :/. That being said, the community is huge, the projects using Stimulus & Turbo are huge - so we'll find a path forward. The technology is just too good not to :).

Edit: For others reading this, the loss of the 2 maintainers was not for any reasons related to the technology itself - but rather due to some ugliness inside of the company Basecamp.

Cheers!

2 Reply
Michael S. Avatar

Great! Really looking forward to this course! :)

Reply
Default user avatar

What about Turbo and Websocket in Symfony instead of ajax?

Reply

Hey Axel!

We'll cover that :). Well, specifically Turbo and Mercure - there is absolutely a built-in feature for having a section of your site subscribe for updates and have them added automatically. It's pretty sweet.

Cheers!

4 Reply
Cat in space

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