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 SubscribePara el día 24, prepárate para una aventura rápida. Hemos aprendido que los Turbo Streams son elementos HTML personalizados que puedes lanzar a la página en cualquier lugar... ¡y se ejecutan! Pero hay otra forma de utilizar los Streams que en realidad está más comúnmente documentada, aunque yo la esté utilizando un poco menos últimamente.
En VoyageController
, desplázate hacia arriba para encontrar la acción new()
. En lugar de redirigir, como hacemos normalmente para el envío de un formulario, la otra opción es devolver una respuesta totalmente llena de flujos Turbo.
Devolver una respuesta de flujos
Observa: quita el flash y devuelve $this->renderBlockView()
... excepto que lo cambias por renderBlock()
. Eso hace lo mismo, pero devuelve un objeto Response
en lugar de una cadena. El último detalle es $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 |
} |
Es un poco técnico, pero esto establecerá una cabecera Content-Type
en la respuesta que le dirá a Turbo:
¡Eh! Ésta no es una respuesta normal de página completa. Sólo devuelvo un conjunto de Turbo Streams que quiero que proceses.
Redoble de tambores, por favor. Actualiza, ve a Nuevo Viaje... rellena los campos... y guarda. ¿Qué ha pasado? El modal sigue abierto y no hay notificación Toast. Pero si has estado atento, ¡la fila de la tabla sí se ha añadido!
En las herramientas de red, busca la petición POST. ¡Fíjate! La respuesta no es más que el <turbo-stream>
: eso es lo único que devolvió nuestra aplicación.
Devolver todos los flujos necesarios
La conclusión es: como no estamos redirigiendo a otra página, ya no obtenemos el comportamiento normal de <turbo-frame>
, en el que encuentra el marco de la página siguiente y lo renderiza. En nuestro caso, el <turbo-frame>
vacío es lo que cerró el modal y renderizó los mensajes flash.
Cuando decides devolver una respuesta de flujo, eres 100% responsable de actualizar todo en la página. Así que, en new.html.twig
, aquí abajo, ¡necesitamos un par de streams más! Abre edit.html.twig
y roba el que cierra el modal. Pon eso aquí.... y luego, desde _frameSuccessStreams.html.twig
, roba el flujo que se anexa al contenedor flash:
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 %} |
¡Creo que eso es todo lo que necesitamos! Inténtalo de nuevo. Aquí está por fin nuestra notificación tostada del envío anterior. Crea un nuevo viaje... y... guarda. Ya está! Notificación tostada, modal cerrado, fila añadida.
Turbo Mercure
Esta idea de devolver sólo un <turbo-stream>
es similar a cómo funciona la integración de Turbo y Mercure. Si no lo sabes, Mercure es una herramienta que te permite obtener actualizaciones en tiempo real en tu front-end... algo así como web sockets, pero más guay. Y Mercure se empareja muy bien con Turbo. Desde dentro de tu controlador, publicas un Update
en Mercure... que se enviará a los frontales de todos los navegadores que estén escuchando este tema chat
.
El contenido de ese Update
es un conjunto de Turbo Streams. Así que publicamos streams... esos streams se envían al frontend a través de Mercure, y Turbo los procesa.
En el frontend, podría tener este aspecto. Editamos una travesía, añadimos unos cuantos signos de exclamación y le damos a guardar. Por supuesto, nuestra página se actualiza gracias a los mecanismos normales de Turbo de los que hemos hablado. Pero, si estuviéramos utilizando Mercure, podríamos hacer que cualquier otra persona de esta página recibiera una actualización de Stream que también dijera que preañadiera esta fila. Así que añado los signos de exclamación, y de repente tú también los ves en tu pantalla, sin refrescar.
Es superguay y funciona mediante Streams.
Vale, aunque esto funciona muy bien, volvamos a nuestra antigua forma... que también funcionaba muy bien. Quita los nuevos Turbo Streams... y deshaz el código en el controlador.
Mañana pasaremos a una de mis partes favoritas de LAST Stack, y la clave para organizar tu sitio en trozos reutilizables: Los Componentes Twig.
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