Cosas extravagantes en el Éxito del Formulario Modal
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 SubscribeHemos estado ocupados. Hemos creado un sistema modal reutilizable basado en AJAX que me encanta. El envío con errores de validación ya funciona. ¿Y el éxito? Ya casi está. Cuando guardamos... no hay notificación de tostada, pero el modal se cerró.
La razón por la que se cerró es importante. En la acción new(), redirigimos a la página índice. Esa página amplía la base.html.twig normal... así que sí tiene una<turbo-frame id="modal"> en ella... pero es esta vacía. Esto significa que el marco modal se vacía, nuestro controlador modal Stimulus se da cuenta y lo cierra.
Planificación: Cuando los formularios están en marcos
En general, cuando añades un <turbo-frame> alrededor de algo -como en la página de inicio con nuestra barra lateral de planetas- tienes que pensar a dónde apuntan los enlaces que hay dentro. Tenemos que asegurarnos de que cada uno vaya a una página que tenga un <turbo-frame> correspondiente.
Cuando un formulario está dentro de <turbo-frame>, tenemos que pensar en lo que ocurre cuando se envía. El caso de error es fácil: siempre muestra la misma página que tiene el mismo marco con los errores dentro. Pero en caso de éxito, tenemos que pensar a dónde redirige el formulario y preguntarnos: ¿tiene esa página un <turbo-frame> que coincida y contiene el contenido correcto?
En el caso de este modal y la página índice, es perfecto: hay un marco coincidente, está vacío y el modal se cierra.
Renderización de Flashes de Éxito con un Turbo Streams
Vale, ¡volvamos a la notificación de tostada que falta! Esta es una situación en la que necesitamos actualizar el <turbo-frame> -para vaciarlo- y también necesitamos actualizar otra área de la página: necesitamos renderizar los mensajes flash de éxito en el contenedor flash.
Esta es una necesidad súper común cuando un formulario se envía dentro de un <turbo-frame>. Así que vamos a resolver esto, creo, de una manera genial y global. Cuando redirijamos con éxito, este <turbo-frame> se cargará finalmente en la página, lo que hará que se cierre el modal. Dentro de él, añade un <turbo-stream> con action="append" ytarget="flash-container":
| <html> | |
| // ... lines 3 - 15 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 17 - 55 | |
| <div | |
| // ... lines 57 - 58 | |
| > | |
| <dialog | |
| // ... lines 61 - 63 | |
| > | |
| <div class="flex grow p-5"> | |
| <div class="grow overflow-auto p-1"> | |
| <turbo-frame | |
| id="modal" | |
| // ... lines 69 - 71 | |
| > | |
| <turbo-stream action="append" target="flash-container"> | |
| // ... line 74 | |
| </turbo-stream> | |
| </turbo-frame> | |
| </div> | |
| </div> | |
| </dialog> | |
| // ... lines 80 - 95 | |
| </div> | |
| </body> | |
| </html> |
Cuando añadimos el sistema de tostado, añadimos un elemento con id="flash-container:
| <html> | |
| // ... lines 3 - 15 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 17 - 51 | |
| <div id="flash-container"> | |
| {{ include('_flashes.html.twig') }} | |
| </div> | |
| // ... lines 55 - 96 | |
| </body> | |
| </html> |
Entonces no lo necesitábamos, pero ahora nos va a venir bien porque podemos apuntar a él para añadirle mensajes flash.
Dentro del flujo, añade la etiqueta template, por supuesto, y luego{{ include('_flashes.html.twig') }}:
| <html> | |
| // ... lines 3 - 15 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 17 - 55 | |
| <div | |
| // ... lines 57 - 58 | |
| > | |
| <dialog | |
| // ... lines 61 - 63 | |
| > | |
| <div class="flex grow p-5"> | |
| <div class="grow overflow-auto p-1"> | |
| <turbo-frame | |
| id="modal" | |
| // ... lines 69 - 71 | |
| > | |
| <turbo-stream action="append" target="flash-container"> | |
| <template>{{ include('_flashes.html.twig') }}</template> | |
| </turbo-stream> | |
| </turbo-frame> | |
| </div> | |
| </div> | |
| </dialog> | |
| // ... lines 80 - 95 | |
| </div> | |
| </body> | |
| </html> |
Esto mostrará los mensajes flash... y el flujo los añadirá a ese contenedor.
¡Vamos a probarlo! Rellena un nuevo viaje, envíalo y... no pasa absolutamente nada. El problema... es sutil. Cuando redirigimos a la página índice, Symfony renderiza toda esa página... aunque Turbo sólo utilizará el <turbo-frame id="modal">. Esto significa que, justo antes de renderizar este código, nuestro contenedor flash renderiza los mensajes flash... lo que los elimina del sistema flash. Así que los mensajes flash están en el HTML que devolvemos de la llamada Ajax... pero como no están dentro del <turbo-frame>, no llegan a la página.
La solución es fácil: asegúrate de que tu contenedor flash está después del modal:
| <html> | |
| // ... lines 3 - 15 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 17 - 51 | |
| <div | |
| data-controller="modal" | |
| data-action="turbo:before-cache@window->modal#close" | |
| > | |
| // ... lines 56 - 91 | |
| </div> | |
| <div id="flash-container"> | |
| {{ include('_flashes.html.twig') }} | |
| </div> | |
| </body> | |
| </html> |
Prueba esto. Actualiza... y rellena el formulario. ¡Ya está! El modal se cierra, ¡y el <turbo-stream> activa el brindis!
¡Y esto es realmente genial! Cuando redirigimos, el <turbo-frame> ahora no está vacío: contiene el flash <turbo-stream>. Pero recuerda: en cuanto se activa un <turbo-stream>, se ejecuta y luego desaparece. Una vez que eso ocurre, el<turbo-frame> queda vacío y el modal se cierra. Eso sí que me gusta.
Extras del flujo: Anteponer la tabla
Lo que me encanta del sistema modal es que funciona... y no hemos necesitado hacer ningún cambio en nuestro controlador. Pero ahora, tenemos que pensar en cualquier comportamiento extra opcional que podamos desear.
Por ejemplo, ¿podríamos anteponer a la tabla el nuevo viaje? Porque, ahora mismo, no lo vemos hasta después de actualizar. Intentémoslo
En index.html.twig, busca el table. Tenemos que preagregarlo en el tbody. Para ello, en el table, añade un id="voyage-list":
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| // ... lines 7 - 21 | |
| <table class="min-w-full bg-gray-800 text-white" id="voyage-list"> | |
| // ... lines 23 - 39 | |
| </table> | |
| </div> | |
| {% endblock %} |
Pensemos: este es otro caso en el que necesitamos actualizar algo que vive fuera de <turbo-frame>. Por tanto, necesitamos un flujo.
Abre new.html.twig y después del bloque body, añade un nuevo bloque llamado stream_success, y después endblock. Dentro, añadiremos los Turbo Streams que necesitemos para que el envío brille de verdad. Añade un <turbo-stream> action="prepend" y luego targets="". La "s" en los objetivos significa que podemos utilizar un selector CSS: #voyage-list tbody. Añade el elemento<template>... y, por ahora, un <tr><td> {{ voyage.purpose }} :
| // ... lines 1 - 24 | |
| {% block stream_success %} | |
| <turbo-stream action="prepend" targets="#voyage-list tbody"> | |
| <template> | |
| <tr><td>{{ voyage.purpose }}</td></tr> | |
| </template> | |
| </turbo-stream> | |
| {% endblock %} |
Vale, ya tenemos un nuevo bloque en nuestra plantilla... que nadie está utilizando. De alguna manera, necesitamos coger este flujo Turbo... y, tras la redirección, renderizarlo en la página siguiente en el modal <turbo-frame>.
¿Cómo lo hacemos? Tenemos dos opciones -y mostraré la segunda el Día 24-. Pero éste es el sistema que me gusta.
En primer lugar, sólo tenemos que preocuparnos de anteponer la fila de la tabla cuando estamos enviando dentro de un <turbo-frame>. Si fuéramos directamente a la página del nuevo viaje -que no tiene marco- y enviáramos, no necesitaríamos ninguna cosa de Turbo Stream. Navegaríamos por la página completa y la renderizaríamos normalmente. Bonito y sencillo.
Así que, en el controlador, empieza con if $request->headers->has('turbo-frame'). Si el envío del formulario se produce dentro de <turbo-frame>, entonces queremos utilizar nuestro flujo. Renderiza ese bloque con $stream y luego con un método de controlador relativamente nuevo: $this->renderBlockView() pasando por voyage/new.html.twig. En lugar de renderizar toda la plantilla, para renderizar un solo bloque pasa esto, lo has adivinado, stream_success. En realidad... Creo que me falta una "s". Mejor.
Pasa a la plantilla una variable voyage.
Para pasar la cadena <turbo-stream> a la página siguiente añádela a un nuevo flash llamadostream:
| // ... lines 1 - 15 | |
| class VoyageController extends AbstractController | |
| { | |
| // ... lines 18 - 25 | |
| ('/new', name: 'app_voyage_new', methods: ['GET', 'POST']) | |
| public function new(Request $request, EntityManagerInterface $entityManager): Response | |
| { | |
| // ... lines 29 - 32 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 34 - 38 | |
| if ($request->headers->has('turbo-frame')) { | |
| $stream = $this->renderBlockView('voyage/new.html.twig', 'stream_success', [ | |
| 'voyage' => $voyage | |
| ]); | |
| $this->addFlash('stream', $stream); | |
| } | |
| // ... lines 46 - 47 | |
| } | |
| // ... lines 49 - 53 | |
| } | |
| // ... lines 55 - 104 | |
| } |
Por último, cuando redirijamos a la página índice y se renderice este <turbo-frame>, haz salir de ese flash: for stream in app.flashes('stream'), endforcon {{ stream|raw }} para que renderice los elementos HTML en bruto:
| <html> | |
| // ... lines 3 - 15 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 17 - 51 | |
| <div | |
| // ... lines 53 - 54 | |
| > | |
| <dialog | |
| // ... lines 57 - 59 | |
| > | |
| <div class="flex grow p-5"> | |
| <div class="grow overflow-auto p-1"> | |
| <turbo-frame | |
| id="modal" | |
| // ... lines 65 - 67 | |
| > | |
| // ... lines 69 - 71 | |
| {% for stream in app.flashes('stream') %} | |
| {{ stream|raw }} | |
| {% endfor %} | |
| </turbo-frame> | |
| </div> | |
| </div> | |
| </dialog> | |
| // ... lines 79 - 94 | |
| </div> | |
| // ... lines 96 - 99 | |
| </body> | |
| </html> |
¡Creo que ya estamos listos! Actualiza... añade un nuevo viaje y... ¡es increíble! La llamada Ajax redirigía a la página índice, donde el marco modal tenía 2 flujos Turbo: uno para renderizar el brindis y otro para preagregar la tabla.
Añadir contenido real
Último paso, preagregar el contenido real. Lo que queremos es este tr. Para obtenerlo desde dentro de new.html.twig, tenemos que aislarlo en su propia plantilla. Cópiala, bórrala e incluye voyage/_row.html.twig:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| class="flex justify-between" | |
| > | |
| // ... lines 10 - 21 | |
| <table class="min-w-full bg-gray-800 text-white" id="voyage-list"> | |
| // ... lines 23 - 30 | |
| <tbody class="divide-y divide-gray-600"> | |
| {% for voyage in voyages %} | |
| {{ include('voyage/_row.html.twig') }} | |
| {% else %} | |
| <tr> | |
| <td colspan="4" class="px-6 py-4 whitespace-nowrap text-center text-gray-400">No records found</td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| {% endblock %} |
Ve a crear esa plantilla... y pégala:
| <tr class="even:bg-gray-700 odd:bg-gray-600"> | |
| <td class="px-6 py-4 whitespace-nowrap">{{ voyage.id }}</td> | |
| <td class="px-6 py-4">{{ voyage.purpose }}</td> | |
| <td class="px-6 py-4 whitespace-nowrap">{{ voyage.leaveAt ? voyage.leaveAt|date('Y-m-d H:i:s') : '' }}</td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <a href="{{ path('app_voyage_show', {'id': voyage.id}) }}" class="text-blue-400 hover:text-blue-600">show</a> | |
| <a href="{{ path('app_voyage_edit', {'id': voyage.id}) }}" class="ml-4 text-yellow-400 hover:text-yellow-600">edit</a> | |
| </td> | |
| </tr> |
Fácil.
Copia la declaración include() y, en new.html.twig, úsala para el flujo:
| // ... lines 1 - 24 | |
| {% block stream_success %} | |
| <turbo-stream action="prepend" targets="#voyage-list tbody"> | |
| <template> | |
| {{ include('voyage/_row.html.twig') }} | |
| </template> | |
| </turbo-stream> | |
| {% endblock %} |
¡Probemos esto! Crea otro viaje y... ¡precioso! El modal se cierra, la notificación tostada se renderiza y la página se actualiza. Es todo lo que queremos.
Mañana vamos a poner a prueba nuestro nuevo sistema modal abriendo el enlace de edición dentro de un modal. Prometí que sería reutilizable, y mañana lo probaremos... con algunas bolas curvas para hacerlo más realista.
24 Comments
About the prepending: wouldn't it be better to update the entire table? In the video's example it works because of how it's sorted but if the table was sorted differently, the prepend would be weird (as the first row wouldn't necessarily be the new voyage).
Hey @kbond!
Yea, it probably would! By prepending the row, I'm sending a smaller payload back... but that's the kind of micro-optimization that I would not worry about at this point anyway :p. However, with both of these solutions, you need to be careful that, if you have pagination, your list is on page... else you're sending back a stream of page 1 to replace page 2 on the page (but as I mentioned, the same problem exists with my prepend row!). I think (though I still need to play with it more and see what patterns develop) the ultimate solution will be in Turbo 8. In Turbo 8, I believe we'll be able to send back a
<turbo-stream action="refresh"></turbo-stream>. This will simply tell whatever page you're on to "refresh". But combined with Turbo 8's morphing, the result should be:A) User makes an AJAX request to "refresh" whatever page they're on
B) The new HTML is "morphed" onto the existing. In other words, only elements that change are updated.
This is, if it all works as advertised, the promise land. To make matters more interesting, if you use Mercure, you could also publish an update to the users on your site with the same stream. For example, if a user is on the
/voyages/5show page, and you update theVoyageentity with id=5, you could send an update to anyone that is on that page that tells them to "refresh". VERY big possibilities with low burden on the dev / complexity.Cheers!
Hi, I came across this comment while trying to reload a table with sorting and pagination after adding an entry. I installed Turbo 8 and experimented with the 'refresh' action along with the
<meta name="turbo-refresh-method" content="morph">, and it works perfectly.However, there's one issue: the success flash message briefly appears but disappears immediately due to the refresh.
From my perspective, a solution would be to create a turbo-frame with a 'src' attribute for the table and move the table to a separate action/template. This frame could then be reloaded using
.reload()- similar to https://github.com/hotwired/turbo/issues/202#issuecomment-1977457984Does anybody has a other/better solution for this?
Ryan,
I saw this when I was looking for something else so I updated to Turbo 8 and used the action="refresh".
You stated:
A) User makes an AJAX request to "refresh" whatever page they're on
B) The new HTML is "morphed" onto the existing. In other words, only elements that change are updated.
This is my situation, I have an 'orders' form, and that form contains different check boxes that one can send the form too, but if someone fills out the 'orders' form, decides they want to add an e-mail to the form through a modal, then the success of that added e-mail form 'refreshes' the page, the contents of the order form are all cleared out from the refresh. I was looking for a solution to this when I found your response about Turbo 8 and it seemed perfect. My 'orders' form is rendered with a LiveComponent, and I use LiveCollection to add rows to the 'orders' form. Ideally what I was looking for was a way to have the success of the e-mail form submit from the modal trigger the re-render of the form through the LiveComponent. Is that possible? This doesn't work but I tried:
I was chasing this solution because I noticed if I fill out an input of the 'orders' form, a LiveUpdate is triggered and the new added e-mail shows up in the list.
Any suggestions are always much appreciated.
Hey @Brandon!
That's an interesting situation! I'm not sure if the
action="refresh"will help you here... though I really haven't played with what this pattern will allow yet.Your idea with live components is compelling. If I understand it correctly, after the user submits the email modal, if the form could be "re-rendered" at that moment, it would now be aware of the new email (because it's in the database) and render differently. Is that correct? If so, yea, asking the live component to re-render is worth a try :).
So, how best to do this? Hmm. Well, it may be overkill, but you can actually create custom stream types - e.g. you could have something like:
I'm not sure I've ever done this, but you would attach some custom JavaScript to this
rerenderaction. There, you could find the target, find the Component for it, then callcompoent.render()on that. Here are some docs on that - https://marcoroth.dev/posts/guide-to-custom-turbo-stream-actionsBecause, ultimately, there is no pure HTML change that you can make that would trigger the live component to re-render. Ultimately, you'll need to execute some JavaScript that then uses JS to re-render the component - https://symfony.com/bundles/ux-live-component/current/index.html#working-with-the-component-in-javascript
Let me know if you have any luck - I'm very curious about this :).
Cheers!
Ryan,
You have it correct, after the user submits the email modal, I would love for the form to re-render at that moment because it is in the database. I was looking for a pure HTML change that would trigger that, but if it doesn't exist I might try a custom stream action. I loathe JavaScript and try to avoid it where I can (that is why I jumped on LiveCollection, I got rid of a whole bunch using that!). I have 5 Stimulus controllers on my site and 4 of them were written by you through all these tutorials. I'll keep you posted on what I can figure out, thank you for all your work. I just finished off a major upgrade to my site following LAST stack tutorials and I have a overwhelming sense of accomplishment.
Hey @Brandon!
Yea, let me know how it goes! And congrats on the upgrade - I can tell you feel like I do when working with this stuff: it just feels right :). Let me know if the stream solution works out.
Cheers!
Hi !
I would like to make some extra validation client side with a stimulus controller like :
But it doesn't work, the response is not send to the <turbo-frame>
Any ideas how to make extra features before submiting stil using <turbo-frame> ?
Hey @ThierryGTH,
It looks like you need to do something like this
I hope it helps!
The submit() call work perfectly but the redirection from controller after form been submitted and validated (
$form->isSubmitted() && $form->isValid()) doesn't go to the turbo-frame.In other words, when you click manually on an submit button without interrupt the submitting process with (event.preventDefault()) every things goes like expected, but if you stop the submit process to do some staff and then submit afterward you are not going to have the same thing like just clicking the submit button without interrupting.
To deal with that, I had to replace my submit button by normal button with an stimulus submit action to fetch the form.
The this.element is the form element (stimulus controller declared in form tag)
Hey @ThierryGTH
I'm afraid I don't fully understand your problem. Is the
submit()call not working?Im hanging around on those flash messages for 2 days now. While normal flash messages work as expected, there is no flash message when the modal dialog is closing. I was rewriting and copy code as said for about 2 days now. Is there any way I can find out where the problem lies?
Hey Nexo,
Difficult to say, you will need to debug some thing deeper. There're a few possible issues:
display: noneCSS property.Try to debug it step by step using
dd(), make sure the flash message is set well with the correct key, made sure it's exist later before you're trying to read it, etc.I hope this help!
Cheers!
Thanks alot for the response, Victor.
While time passed, I found the problem. As a newbie with turbo, I got a bit confused about the turbo frames and how everything works together. The flash message got written just before the modal closed. At the end it was a matter of how and where to put that flash messages.
Thank you very much for the great tutorials. Keep up the good work.
Cheers
Nexo
Hey Nexo,
Awesome, I'm glad you figured it out yourself, well done! Yeah, all those Turbo-related topics are something new and it's easy to miss something simple. You just need to get used to it with more practice ;)
Cheers!
Sorry, I am newbie to the symfony and programming. I am curious is it should be so that select list opens inside of the modal and doesn`t overflow it?
I just find it not very comfortable, when the full list is not visible, and you don't see all the options.
Hey BlackRock,
Your problem sounds like a CSS problem in general, and I bet it could be fixed in CSS. But to understand it better, could you share a screenshot of your problem? You can upload it somewhere to image hosting like Imgur and send the link to it in the comment because we do not allow upload images directly to comments. Because I'm not sure I completely understand the problem. Do you have a behavior that is different from the one you see on this video?
Cheers!
Yes, I understand that this can be solved with CSS. I thought that this is obvious, and it should be written in the tutorial if it is important. That is why I am asking.
I am doing everything according to the text version of the tutorial.
Here is the link on the screen gif: https://i.imgur.com/CD11EfD.gif
Thank you!
Hey BlackRock,
Thank you for sharing a screenshot, it helped a lot to understand the problem. Yeah, I see what you mean, not perfect, but unfortunately, that's how HTML/CSS works in the current situation. The problem is that the dropdown list is added as an overlay over the other form HTML code, and so it does not push the dialog height. Fairly speaking, I'm not sure what would be the best workaround, but there're some that you may consider:
I hope this helps!
Cheers!
I added class 'overflow-visible' to the <dialog> and removed class 'overflow-auto' in the inner <div>. And everything worked
Hey @BlackRock!
I wonder if I was kind of solving the same issue in the last chapter - https://symfonycasts.com/screencast/last-stack/flowbite#making-the-modal-click-outside-smarter
I'm not actually changing the overflow behavior. But I was fixing a problem caused by something overflowing off of the bottom. Getting the overflow behavior correct was something I worked on quite a bit - but it wouldn't surprise me if it's not perfect (as it was complex).
What is the behavior when you changed to
overflow-auto? I just tried it and I didn't notice a difference - but perhaps I'm not looking closely enough :).Cheers!
Hey @weaverryan!
Sorry for some delay with the answer. Not sure if you have the same issue in the course. Because you write there:
So I assume, that you can see datepicker fully. The main issue is that it extends modal, but triggers <dialog>.
My issue is that I cannot see datepicker fully. Part of it hides inside of the <dialog>: https://i.imgur.com/yRsAacL.gif
This is the behavior if I leave all the tailwind classes as they are in the course.
If I add class 'overflow-visible' to the <dialog> and leave the class 'overflow-auto' in the inner <div>, then it won't limit datepicker, but will limit planets select list.
That is why I delete 'overflow-auto'.
Hey @BlackRock!
Sorry for the slow reply also on my end :). The video is super helpful! It turns out that I wasn't seeing the issue simply because I was using my touch pad to scroll vs actually trying to click on the scroll bar. Indeed, clicking on the scrollbar has the same effect for me.
I think the problem with your solution is that, if the content gets even longer, it will continue outside of the modal, without any scrolling. In this screenshot, I've duplicated the header a bunch of times - you can see it's starting to exit the modal. And there are even more headers below this that we can't see or scroll to https://imgur.com/z4urzkO
So, yes, there IS a problem here, but that's why I'm wondering if https://symfonycasts.com/screencast/last-stack/flowbite#making-the-modal-click-outside-smarter is actually the solution: it's expected that the datepicker won't be shown fully because the modal isn't big enough. But that fix should (I hope) allow you to scroll down. And, btw, in this example, another real-world solution might be to make the modal bigger - there's a lot of extra space.
Overall, I'm still not 100% satisfied with the overflow... but not sure what the perfect solution is yet.
Cheers!
Hey @weaverryan:)
If the content gets even longer, we can add some css to scroll these options.
Something like this:
Frankly, I doubt that https://symfonycasts.com/screencast/last-stack/flowbite#making-the-modal-click-outside-smarter is the solution exactly for the problem that I have. Because this code just makes sure that the click is inside the element, and nothing else.
"Houston: no signs of life"
Start the conversation!