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!
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Keep on Learning!
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 SubscribeFor day 24, strap in for a quick adventure. We've learned that Turbo Streams are custom HTML elements that you can throw onto the page anywhere... and they execute! But there's another way to use Streams that's actually more commonly-documented, even if I'm using it a bit less lately.
In VoyageController
, scroll up to find the new()
action. Instead of redirecting, like we normally do for a form submit, the other option is to return a response that is entirely filled with Turbo streams.
Returning a Response of Streams
Watch: remove the flash and return $this->renderBlockView()
... except change it to renderBlock()
. That does the same thing, but returns a Response
object instead of a string. The last detail is $request->setRequestFormat()
TurboBundle::STREAM_FORMAT
:
Show Lines
|
// ... lines 1 - 13 |
use Symfony\UX\Turbo\TurboBundle; | |
Show Lines
|
// ... lines 15 - 16 |
class VoyageController extends AbstractController | |
{ | |
Show Lines
|
// ... lines 19 - 27 |
public function new(Request $request, EntityManagerInterface $entityManager): Response | |
{ | |
Show Lines
|
// ... lines 30 - 33 |
if ($form->isSubmitted() && $form->isValid()) { | |
Show Lines
|
// ... lines 35 - 39 |
if ($request->headers->has('turbo-frame')) { | |
$request->setRequestFormat(TurboBundle::STREAM_FORMAT); | |
return $this->renderBlock('voyage/new.html.twig', 'stream_success', [ | |
'voyage' => $voyage | |
]); | |
} | |
Show Lines
|
// ... lines 47 - 48 |
} | |
Show Lines
|
// ... lines 50 - 54 |
} | |
Show Lines
|
// ... lines 56 - 121 |
} |
It's a bit techy, but this will set a Content-Type
header on the response that tells Turbo:
Hey! This is not a normal full page response. I'm returning just a set of Turbo Streams that I want you to process.
Drumroll, please. Refresh, go to New Voyage... fill out the fields... and save. What happened? The modal is still open and no Toast notification. But if you were watching closely, the row in the table did prepend!
In the network tools, find the POST request. Look at that! The response is nothing more than the <turbo-stream>
: that's the only thing our app returned.
Returning All the Streams Needed
The takeaway is: because we're not redirecting to another page, we no longer get the normal <turbo-frame>
behavior where it finds the frame on the next page and renders that. In our case, the empty <turbo-frame>
is what closed the modal and rendered the flash messages.
When you decide to return a stream response, you are 100% responsible for updating everything on the page. So, in new.html.twig
, down here, we need a couple more streams! Open edit.html.twig
and steal the one that closes the modal. Pop that here.... then, from _frameSuccessStreams.html.twig
, steal the stream that appends to the flash container:
Show Lines
|
// ... lines 1 - 24 |
{% block stream_success %} | |
<turbo-stream action="prepend" targets="#voyage-list tbody"> | |
<template> | |
{{ include('voyage/_row.html.twig') }} | |
</template> | |
</turbo-stream> | |
<turbo-stream action="update" target="modal"> | |
<template></template> | |
</turbo-stream> | |
<turbo-stream action="append" target="flash-container"> | |
<template>{{ include('_flashes.html.twig') }}</template> | |
</turbo-stream> | |
{% endblock %} |
I think that's all we need! Give this another shot. Here's our toast notification finally from the previous submit. Create a new voyage... and ... save. That's it! Toast notification, modal closed, row prepended.
Turbo Mercure
This idea of returning just a <turbo-stream>
is similar to how the Turbo and Mercure integration works. If you don't know, Mercure is a tool that allows you to get real-time updates on your front end... kind of like web sockets, but cooler. And Mercure pairs really well with Turbo. From inside your controller, you publish an Update
to Mercure... which will be sent to the frontends of every browser that's listening to this chat
topic.
The content of that Update
is a set of Turbo Streams. I'll scroll down to that template. So we publish streams... those streams are sent to frontend via Mercure, and Turbo processes them.
On the frontend, it might look like this. We edit a voyage, add a few exclamation points and hit save. Of course, our page updates thanks to the normal Turbo mechanisms we've talked about. But, if we were using Mercure, we could make it so that anyone else on this page could receive a Stream update that also says to prepend this row. So I add the exclamation points, and you suddenly also see them on your screen, without refreshing.
It's super cool and powered via Streams.
Ok, even though this is working nicely, let's go back to our old way... which was also working nicely. Remove the new Turbo Streams... and undo the code in the controller.
Tomorrow, we move on to one of my favorite parts of LAST Stack - and the key to organizing your site into reusable chunks: Twig Components.
20 Comments
Hey @ik3rib
I believe your editor is being re-instantiated on a re-render. Can you confirm?
Also, have you tried not to validate the form on every input change? (I watched the video and believe you don't need that feature)
Cheers!
I "fixed" it with:{{ form_start(form, {attr: {'data-model': 'norender|*'}}) }}
The problem is not with validation...
But I see the problem is when I change input (POST occurs) but there is no data. If I have a date on the datepicker (startDate) on the instantiateForm the date value (on startDate) is empty so when the form is rerendered the input becomes empty. I also see that if you select a date on the datepicker or change the trix editor, post action is not triggered.
The other problem is with the trix editor, it goes away when livecomponent form is rerendered and I don't know how to fix this issue.
As I said, it works ok with this fix, but it would be nice to know how to fix the other things...
Maybe I need an stimulus controller to set the value for the date field (startDate) and the textarea?
That's interesting. When the component re-renders, are those fields (date and textarea) re-initialized? I'd like to discover why the fields are emptied.
I suppose the post action is not triggered because the original input element is replaced by something else via JS (thanks to the datepicker and trix editor libraries). I think you should take control of those fields with a Stimulus controller. You can learn more here: https://symfony.com/bundles/ux-live-component/current/index.html#working-with-the-component-in-javascript
Cheers!
I created a repo with a demo where you can see the error in action
https://github.com/ikerib/last_stack_trix_datetime_error_demo
Ok, I dug in and realized that the Trix editor is not compatible at all with Live components, so you need to add data-live-ignore
attribute to the parent element of the input field. It tells the Live component to ignore it completely
More info here: https://symfony.com/bundles/ux-live-component/current/index.html#skipping-updating-certain-elements
And about Datepicker, it requires some custom code to make it work. The problem is the date field does not trigger a normal change
event so Symfony UX can detect it and update the component's model, so you have to do it manually. You need to find a way to listen when the Datepicker field changes and update the component model manually (via JS). You can read more about updating the model here: https://symfony.com/bundles/ux-live-component/current/index.html#updating-a-model-manually
I hope it helps. Cheers!
Sorry for my late reply, I've been busy, but I'll check it this week. Cheers!
Hello everyone,
I would like to describe a problem that I noticed when working with modals and AjaxForms and UX Turbo. I want to do the following:
In the website there is a button in the header that opens a modal/dialog - built as described here. On every page (route) of the application I can open this modal, which contains a form for a new record.
Now I want the record to be saved and redirected to a specific route after submitting the form - regardless of where I opened it from.
For example: I open the modal/dialog from the home page (route app_homepage). I create the record via the modal, submit the form and then want the redirect to the route app_newsevent_default_index to be executed after saving.
Unfortunately that doesn't work. If I use the modal from the home page, I end up back on the home page after submitting. If I open the modal from the route app_admin_user, for example, I end up back on the route app_admin_user after submitting.
Here is the code of the controller for the post request that is triggered when the form is submitted:
public function new(Request $request, NewseventManager $newseventManager, EntityManagerInterface $entityManager): Response
{
$newsevent = new Newsevent();
$form = $this->createNewseventForm($newsevent);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$newsevent->setCreatedFrom($this->getAppUser());
$newsevent->setEventnumber($newseventManager->getNextEventNumber($newsevent));
$newsevent->setYear($newseventManager->getCurrentYear());
$entityManager->persist($newsevent);
$entityManager->flush();
$this->addFlash('success', 'Ereignis erfolgreich erstellt');
if ($request->headers->has('turbo-frame')) {
$stream = $this->renderBlockView('newsevent/default/new.html.twig', 'stream_success', [
'newsevent' => $newsevent,
]);
$this->addFlash('stream', $stream);
}
return $this->redirectToRoute('app_newsevent_default_index', [], Response::HTTP_SEE_OTHER);
}
return $this->render('newsevent/default/new.html.twig', [
'newsevent' => $newsevent,
'form' => $form->createView(),
]);
}
Here is the code from the template:
{% extends 'modalBase.html.twig' %}
{% block title %}Newsereignis hinzufügen...{% endblock %}
{% block body %}
<div class="m-4 p-4 modal:m-0 modal:p-0 bg-white dark:bg-gray-800 rounded-lg">
<div class="flex justify-between">
<h1 class="text-xl font-semibold mb-4">Neues Newsereignis</h1>
<div class="mr-4">
<a href="{{ path('app_newsevent_default_index') }}" class="text-sm font-medium">
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="m15 9-6 6m0-6 6 6m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
</svg>
</a>
</div>
</div>
{{ include('newsevent/default/_form_new.html.twig') }}
</div>
{% endblock %}
{% block stream_success %}
<turbo-stream action="prepend" targets="#newslist-list tbody">
<template>
{{ include('newsevent/default/_row.html.twig') }}
</template>
</turbo-stream>
{% endblock %}
Have you any ideas?
Thanx and Cheers :)
Hey @creativx007
It's hard to tell what's causing that unexpected behavior from Turbo, the only thing that catches my attention is the Response status code, try with 301, or 302
Cheers!
Hey @MolloKhan : I tried both - unfortunately without success.
The redirect itself is already responsive, but the target doesn't match. If I remove the redirect in the controller, the data record is saved, but the modal/dialog does not close. Do you have another idea? Would be important :)
Cheers!
If I recall correctly, the modal closes automatically when its content is cleared out, so you only need to empty the modal's content. Your response should return a turbo stream with the same modal's id but with no HTML. Here's the chapter where Ryan implements that functionality https://symfonycasts.com/screencast/last-stack/modal#script
Hope it helps!
Many thanks for your response. But unfortunately that doesn't help me. I can't find a solution based on your suggestion.
I'll try to summarize it again:
I have a route (/newsevent). Here is a list of all news events. Starting from this route, I open the modal to create a new news event. I fill out the modal, click save. The data record is created and the modal closes. The list of news events is automatically updated with the new entry.
I click on a different route (e.g. /admin/user). There is also a button there that opens the modal for a new news event. I click, the modal opens, I enter the data and click save. The modal closes and I'm stuck on the /admin/user route.
So there is no real redirect to the /newsevent route.
How can I force the real redirect here - The goal is to create a news event from anywhere on the website and to land on the /newsevent route after creation.
Do you have any idea?
Cheers!
Hi everyone, do I still have a chance of finding a solution? As described, the redirect isn't working. Isn't there a way to tell the redirect to really reload the requested page? I urgently need a solution here. Thanks :)
Cheers!
Hey @creativx007!
Sorry for the slow reply: the team kept this more challenging question for me, and I'm not often available, unfortunately.
What you're trying to do makes perfect sense, but is non-standard, so let's think. What about a custom Turbo Stream? This is for Turbo 7, but should still be relevant: https://marcoroth.dev/posts/guide-to-custom-turbo-stream-actions
The idea is:
A) Create a custom stream action called redirect
with something like href="/newsevent"
B) You add this to your response when an event is created from the admin area
C) In your JS that handles this action, you read off the redirect
attribute and redirect there in JS (or more likely, you do a Turbo.visit(theHref)
.
Let me know if this helps! It's a great question. Sorry again about the delay!
Cheers!
Hey Ryan, thank you very much for your answer. And no problem, because I'm happy to wait for such a professional and helpful answer :)
It works - I can now set a redirect to a desired page:
in templalte /new.html.twig
{% block stream_success %}
<turbo-stream action="prepend" targets="#organisationgroup-list tbody">
<template>
{{ include('swd/admin/organisation-group/_row.html.twig') }}
</template>
</turbo-stream>
<turbo-stream action="update" target="modal">
<template></template>
</turbo-stream>
<turbo-stream action="redirect" href="/swd"></turbo-stream>
<turbo-stream action="append" target="flash-container">
<template>{{ include('_flashes.html.twig') }}</template>
</turbo-stream>
{% endblock %}
in app.js:
import { StreamActions } from "@hotwired/turbo"
StreamActions.redirect = function() {
const href = this.getAttribute("href")
Turbo.visit(href)
}
It works wonderfully. What I haven't managed to do yet is display the Flash Message. I specifically placed the block with FlashMessage after the redirect in the stream block - but that probably can't work like that.
Can I still activate the FlashMessage in my own JS in the app.js or will it be gone because of the redirect?
Cheers and thanks :)
Hey @creativx007 ,
Sorry about the long reply. I'm glad to hear Ryan's answer helped to you :)
About the redirect, it depends if the flash message was rendered or no. As long as it's not rendered yet and you redirect to another page where it's actually should be rendered - then it will work. But if before the actual redirect the page that is loaded rendered the flash message -then it won't be rendered on the redirected page and you need to find some workarounds to make it work, e.g. do not render the flash message before the actual redirect.
Cheers!
@lexhartman Thank you! <3
Hey @lexhartman ,
Thank you for this feedback, we are really happy to hear you love it! ❤️
Stay tuned, more redesigned pages are coming soon ;)
Cheers!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "4.x-dev", // 4.x-dev
"doctrine/doctrine-bundle": "^2.10", // 2.12.x-dev
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.4.x-dev
"doctrine/orm": "^2.16", // 2.18.x-dev
"knplabs/knp-time-bundle": "dev-main", // dev-main
"pagerfanta/doctrine-orm-adapter": "4.x-dev", // 4.x-dev
"pagerfanta/twig": "4.x-dev", // 4.x-dev
"symfony/asset": "6.4.*", // 6.4.x-dev
"symfony/asset-mapper": "6.4.*", // 6.4.x-dev
"symfony/console": "6.4.x-dev", // 6.4.x-dev
"symfony/dotenv": "6.4.x-dev", // 6.4.x-dev
"symfony/flex": "^2", // 2.x-dev
"symfony/form": "6.4.x-dev", // 6.4.x-dev
"symfony/framework-bundle": "6.4.x-dev", // 6.4.x-dev
"symfony/monolog-bundle": "^3.0", // dev-master
"symfony/runtime": "6.4.x-dev", // 6.4.x-dev
"symfony/security-csrf": "6.4.x-dev", // 6.4.x-dev
"symfony/stimulus-bundle": "2.x-dev", // 2.x-dev
"symfony/twig-bundle": "6.4.x-dev", // 6.4.x-dev
"symfony/ux-autocomplete": "2.x-dev", // 2.x-dev
"symfony/ux-live-component": "2.x-dev", // 2.x-dev
"symfony/ux-turbo": "2.x-dev", // 2.x-dev
"symfony/ux-twig-component": "2.x-dev", // 2.x-dev
"symfony/validator": "6.4.x-dev", // 6.4.x-dev
"symfony/web-link": "6.4.*", // 6.4.x-dev
"symfony/yaml": "6.4.x-dev", // 6.4.x-dev
"symfonycasts/dynamic-forms": "dev-main", // dev-main
"symfonycasts/tailwind-bundle": "dev-main", // dev-main
"tales-from-a-dev/flowbite-bundle": "dev-main", // dev-main
"twig/extra-bundle": "^2.12|^3.0", // 3.x-dev
"twig/twig": "^2.12|^3.0" // 3.x-dev
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.6.x-dev
"phpunit/phpunit": "^9.5", // 9.6.x-dev
"symfony/browser-kit": "6.4.*", // 6.4.x-dev
"symfony/css-selector": "6.4.*", // 6.4.x-dev
"symfony/debug-bundle": "6.4.x-dev", // 6.4.x-dev
"symfony/maker-bundle": "^1.51", // dev-main
"symfony/panther": "^2.1", // v2.1.1
"symfony/phpunit-bridge": "7.1.x-dev", // 7.1.x-dev
"symfony/stopwatch": "6.4.x-dev", // 6.4.x-dev
"symfony/web-profiler-bundle": "6.4.x-dev", // 6.4.x-dev
"zenstruck/browser": "1.x-dev", // 1.x-dev
"zenstruck/foundry": "^1.36" // 1.x-dev
}
}
does anyone use a trix editor on a modal with a form as live component?
I followed the last stack tutorial, everything is working great but I realize that in my form when validator ocurs and the componente is rerendered the trix editor is gone... anyone knows how to fix it?
The same happens with the datepicker....
I uploaded a video showing the problem...
https://symfony-devs.slack.com/files/U3FP9BW6S/F07S1GT5WGH/trix.mp4