Buy Access to Course
22.

Cosas extravagantes en el Éxito del Formulario Modal

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Hemos 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":

99 lines | templates/base.html.twig
<!DOCTYPE html>
<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:

99 lines | templates/base.html.twig
<!DOCTYPE html>
<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') }}:

99 lines | templates/base.html.twig
<!DOCTYPE html>
<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:

99 lines | templates/base.html.twig
<!DOCTYPE html>
<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":

43 lines | templates/voyage/index.html.twig
// ... 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 }} :

32 lines | templates/voyage/new.html.twig
// ... 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:

106 lines | src/Controller/VoyageController.php
// ... lines 1 - 15
class VoyageController extends AbstractController
{
// ... lines 18 - 25
#[Route('/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:

102 lines | templates/base.html.twig
<!DOCTYPE html>
<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:

43 lines | templates/voyage/index.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:

32 lines | templates/voyage/new.html.twig
// ... 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.