¡Más con Modales divertidos! Editar y borrar
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 SubscribeBienvenido al día 23, el gran final de nuestra saga de sistemas modales. Aunque volveremos sobre ello dentro de unos días, cuando hablemos de los componentes Twig.
Así que si nuestro nuevo sistema modal es tan reutilizable como he prometido, también deberíamos poder abrir fácilmente el formulario de edición en un modal, ¿verdad?
Abrir el formulario de edición en un modal
Para optar por el sistema modal, lo único que tenemos que cambiar -enedit.html.twig
- es ampliar modalBase.html.twig
. Y ya que estamos aquí, quita el relleno extra con modal:m-0
y modal:p-0
:
{% extends 'modalBase.html.twig' %} | |
// ... lines 2 - 4 | |
{% block body %} | |
<div class="m-4 p-4 modal:m-0 modal:p-0 bg-gray-800 rounded-lg"> | |
// ... lines 7 - 22 | |
</div> | |
{% endblock %} |
A continuación, haz que el enlace de edición se dirija al marco modal
. Esto está en _row.html.twig
. Lo dividiré en varias líneas: .... y luego añadiré data-turbo-frame="modal"
:
<tr class="even:bg-gray-700 odd:bg-gray-600"> | |
// ... lines 2 - 4 | |
<td class="px-6 py-4 whitespace-nowrap"> | |
// ... line 6 | |
<a | |
href="{{ path('app_voyage_edit', {'id': voyage.id}) }}" | |
class="ml-4 text-yellow-400 hover:text-yellow-600" | |
data-turbo-frame="modal" | |
>edit</a> | |
</td> | |
</tr> |
Momento de la verdad. Actualiza. Y... ¡maldita sea! ¡Simplemente funciona! Incluso si guardamos con éxito, funciona. Obtenemos la tostada, el modal se cierra, ¡madre mía!
Esto funciona porque, en VoyageController
, la acción edit
, al igual que new
, redirige a la página index
:
// ... lines 1 - 15 | |
class VoyageController extends AbstractController | |
{ | |
// ... lines 18 - 64 | |
public function edit(Request $request, Voyage $voyage, EntityManagerInterface $entityManager): Response | |
{ | |
// ... lines 67 - 69 | |
if ($form->isSubmitted() && $form->isValid()) { | |
// ... lines 71 - 74 | |
return $this->redirectToRoute('app_voyage_index', [], Response::HTTP_SEE_OTHER); | |
} | |
// ... lines 77 - 81 | |
} | |
// ... lines 83 - 104 | |
} |
Que tiene un marco modal vacío, por lo que el modal se cierra.
Cuando el modal no se cierra
Pero... Quiero ser delicado. El formulario de edición aparece ahora en dos contextos, el modal, pero también en su página independiente. ¿Qué pasa si, cuando estamos en esta página, al tener éxito, queremos redirigirnos de nuevo aquí para poder seguir editando?
¡Es fácil! Cambia la ruta a app_voyage_edit
y pon id
a $voyage->getId()
:
// ... lines 1 - 15 | |
class VoyageController extends AbstractController | |
{ | |
// ... lines 18 - 64 | |
public function edit(Request $request, Voyage $voyage, EntityManagerInterface $entityManager): Response | |
{ | |
// ... lines 67 - 69 | |
if ($form->isSubmitted() && $form->isValid()) { | |
// ... lines 71 - 74 | |
return $this->redirectToRoute('app_voyage_edit', ['id' => $voyage->getId()], Response::HTTP_SEE_OTHER); | |
} | |
// ... lines 77 - 81 | |
} | |
// ... lines 83 - 104 | |
} |
Genial. Ahora, cuando guardemos, ¡funcionará! Pero... ¿cómo ha afectado eso al formulario del modal? Cuando editamos y guardamos... no pasa nada. El modal sigue aquí y no hay notificación de tostado.
Renderizar los "flujos de fotogramas" en todos los fotogramas
Trabajemos primero en la notificación de brindis que falta. En base.html.twig
, dentro del marco modal
, renderizamos los mensajes flash en un <turbo-stream>
. El problema es que... cuando redirigimos a la página de edición, como ésta extiendemodalBase.html.twig
, el marco que se devuelve es éste. Y este<turbo-frame>
no renderiza estos flujos.
Resulta que, en realidad, estas líneas deberían vivir dentro de cualquier <turbo-frame>
que pudiera renderizarse tras el envío de un formulario.
Para ello, copia esto y, dentro del directorio templates/
, crea un nuevo archivo llamado _frameSuccessStreams.html.twig
. Pégalo dentro:
<turbo-stream action="append" target="flash-container"> | |
<template>{{ include('_flashes.html.twig') }}</template> | |
</turbo-stream> | |
{% for stream in app.flashes('stream') %} | |
{{ stream|raw }} | |
{% endfor %} |
Pero antes de usarlo, quiero añadir otro detalle:if app.request.headers.get('turbo-frame')
es igual a una nueva variable frame
, entonces renderiza esto, si no, no hagas nada:
{% if app.request.headers.get('turbo-frame') == frame %} | |
<turbo-stream action="append" target="flash-container"> | |
<template>{{ include('_flashes.html.twig') }}</template> | |
</turbo-stream> | |
{% for stream in app.flashes('stream') %} | |
{{ stream|raw }} | |
{% endfor %} | |
{% endif %} |
Estoy codificando para un caso extremo, así que deja que me explique. Imagina que tenemos dos elementos<turbo-frame>
en la misma página: id="login"
yid="registration"
. Y ambos incluyen este parcial. En este caso, el <turbo-frame id="login">
siempre renderizaría los mensajes flash... sin dejar nada para el pobre marco registration
. Y así, cuando estamos enviando dentro del marco registration
Turbo Frame... no veríamos las notificaciones del brindis.
Para solucionarlo, cuando utilicemos este parcial - include('_frameSuccessStreams.html.twig')
- pasa el nombre del marco dentro del que estás: modal
:
<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 | |
// ... lines 64 - 67 | |
> | |
{{ include('_frameSuccessStreams.html.twig', { frame: 'modal' }) }} | |
</turbo-frame> | |
</div> | |
</div> | |
</dialog> | |
// ... lines 74 - 89 | |
</div> | |
// ... lines 91 - 94 | |
</body> | |
</html> |
De esta forma, si el marco actual es otro, esto no se comerá los mensajes flash.
Copia esto, y en modalFrame.html.twig
, pégalo aquí también:
<turbo-frame id="modal"> | |
{% block body %}{% endblock %} | |
{{ include('_frameSuccessStreams.html.twig', { frame: 'modal' }) }} | |
</turbo-frame> |
¡Hagámoslo! Actualiza, Edita... y guarda. El modal sigue abierto, pero mira ahí detrás: ¡vemos la tostada!
Cerrar el modal cuando quiere permanecer abierto
Ahora: ¿cómo podemos cerrar este molesto modal? Cuando ponemos un formulario dentro de un marco, puede que nuestro controlador Symfony no necesite cambiar. Los mensajes Flash funcionarán y, dependiendo de dónde redirijas, el modal podría incluso cerrarse.
Pero tienes que preguntarte: ¿dónde están todos los lugares en los que se utilizará mi formulario? y: ¿estoy devolviendo la respuesta adecuada para cada situación? Ahora mismo, en la situación del modal, nuestra respuesta no es la que queremos: no hace que el modal se cierre.
¡Y no pasa nada! Recuerda: además de dejar que el marco Turbo se actualice con el contenido tras la redirección, también podemos utilizar streams para hacer cualquier cosa extra.
En new.html.twig
, roba el stream_success
de la parte inferior. En edit.html.twig
, pega. Esta vez, queremos actualizar el elemento <turbo-frame id="modal">
para vaciar su contenido y que el modal se cierre. Hazlo con action="update"
,target="modal"
, y pon el <template>
a nada:
// ... lines 1 - 25 | |
{% block stream_success %} | |
<turbo-stream action="update" target="modal"> | |
<template></template> | |
</turbo-stream> | |
{% endblock %} |
En el controlador, para añadir la "cosa extra", copia la sentencia if denew
... pégala aquí abajo, cambia la plantilla a edit.html.twig
y... ¡ya deberíamos estar bien!
// ... lines 1 - 15 | |
class VoyageController extends AbstractController | |
{ | |
// ... lines 18 - 64 | |
public function edit(Request $request, Voyage $voyage, EntityManagerInterface $entityManager): Response | |
{ | |
// ... lines 67 - 69 | |
if ($form->isSubmitted() && $form->isValid()) { | |
// ... lines 71 - 73 | |
if ($request->headers->has('turbo-frame')) { | |
$stream = $this->renderBlockView('voyage/edit.html.twig', 'stream_success', [ | |
'voyage' => $voyage | |
]); | |
$this->addFlash('stream', $stream); | |
} | |
// ... lines 81 - 82 | |
} | |
// ... lines 84 - 88 | |
} | |
// ... lines 90 - 111 | |
} |
Vale, dale a "Editar" y guarda. Hmm, he visto el brindis, pero el modal no se ha cerrado. Déjame ver el flujo para asegurarme de que lo tengo todo. ¡Ah! Con targets
, utilizas un selector CSS. Pero con target
, es sólo el id:
// ... lines 1 - 25 | |
{% block stream_success %} | |
<turbo-stream action="update" target="modal"> | |
// ... line 28 | |
</turbo-stream> | |
{% endblock %} |
Así que el Turbo Stream se estaba ejecutando... pero no coincidía con nada.
Intentémoslo de nuevo. Cuando le demos a guardar, eso nos redirigirá de nuevo a la página de edición, y eso va a tener un <turbo-frame id="modal">
con contenido: no estará vacío. Pero entonces, nuestro flujo debería vaciarlo y el modal debería cerrarse.
Y... ¡precioso!
Actualizar la fila en Editar
¿Puedo añadir un último detalle para pulir la edición? Sería genial que, cuando cambiáramos un viaje, actualizara la fila al instante. Este es otro "extra", y... va a ser fácil.
Primero, para apuntar esto, en _row.html.twig
, añade un id
,voyage-list-item-
, {{ voyage.id }}
:
<tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}"> | |
// ... lines 2 - 12 | |
</tr> |
Copia eso, dirígete a edit.html.twig
y añade un Turbo Stream más:action="replace"
y target="voyage-list-item-"
voyage.id
. Añade el<template>
y luego incluye voyage/_row.html.twig
:
// ... lines 1 - 25 | |
{% block stream_success %} | |
// ... lines 27 - 29 | |
<turbo-stream action="replace" target="voyage-list-item-{{ voyage.id }}"> | |
<template>{{ include('voyage/_row.html.twig') }}</template> | |
</turbo-stream> | |
{% endblock %} |
Aquí es donde las cosas realmente empiezan a brillar. Edita, elimina esos signos de exclamación y... la página se actualiza al instante. Nuestro modal de edición -incluso con todas las complicaciones que le he echado- ¡está hecho!
Cómo eliminar
Con nuestros últimos 3 minutos, asegurémonos de que el botón "eliminar" funciona. Oh... ¡funciona! ¡El modal se cierra y aparece la tostada! Como las otras acciones, después de borrar, redirige a la página index
y el marco vacío modal
cierra el modal. ¡Es genial!
Excepto... que la fila que he borrado sigue ahí hasta que actualizamos.
Pero espera. El botón de borrar es un formulario que se envía. Y la única razón por la que se envía a <turbo-frame>
es porque está dentro del marco modal.
Pero la acción de eliminar no necesita enviarse a un marco. Nunca vamos a hacer clic en "Eliminar" y luego querremos mostrar algo en el modal. Una navegación por toda la página estaría bien.
Para ello, en _delete_form.html.twig
, en el marco, añade data-turbo-frame="_top"
:
<form method="post" data-turbo-frame="_top" action="{{ path('app_voyage_delete', {'id': voyage.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');"> | |
// ... lines 2 - 5 | |
</form> |
La redirección provoca una navegación a página completa, lo que está bien.
Supresión extrafantástica
Aunque, sí, podría ser más suave. Desplázate un poco hacia abajo... y borra uno. La página vuelve al principio.
Como con todo, si esto es importante para nosotros, podemos mejorarlo. Elimina el data-turbo-frame="_top"
:
<form method="post" action="{{ path('app_voyage_delete', {'id': voyage.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');"> | |
// ... lines 2 - 5 | |
</form> |
Cuando un formulario -incluso nuestro formulario de eliminación- existe dentro de un <turbo-frame>
, tenemos que preguntarnos: ¿dónde se está utilizando esto y qué tengo que actualizar para que la página sea perfecta después del éxito? En este caso, necesitamos eliminar la fila. Así que tenemos que hacer algo extra, fuera del marco. ¡Y ya sabemos cómo hacerlo!
En edit.html.twig
, roba el bloque stream_success
. Luego crea una nueva plantilla llamada delete.html.twig
. Eliminar no suele tener su propia plantilla... y vamos a utilizarla sólo para stream_success
. Utiliza ésta, cambia action
por remove
y target
voyage-list-item-
pero sólo utiliza una variable id
. Y para eliminar, no necesitamos el <template>
en absoluto:
{% block success_stream %} | |
<turbo-stream action="remove" target="voyage-list-item-{{ id }}"></turbo-stream> | |
{% endblock %} |
En VoyageController
, desplázate hacia arriba, roba la declaración if.... y abajo en eliminar, pega eso. Cambia la plantilla a delete.html.twig
y pasa una variable id
establecida en $id
. No podemos usar $voyage->getId()
porque ya estará vacía desde que la borramos. En su lugar, pasa $id
... y antes de borrar, establece que:$id = $voyage->getId()
:
// ... lines 1 - 15 | |
class VoyageController extends AbstractController | |
{ | |
// ... lines 18 - 91 | |
public function delete(Request $request, Voyage $voyage, EntityManagerInterface $entityManager): Response | |
{ | |
if ($this->isCsrfTokenValid('delete'.$voyage->getId(), $request->request->get('_token'))) { | |
$id = $voyage->getId(); | |
// ... lines 96 - 100 | |
if ($request->headers->has('turbo-frame')) { | |
$stream = $this->renderBlockView('voyage/delete.html.twig', 'success_stream', [ | |
'id' => $id, | |
]); | |
$this->addFlash('stream', $stream); | |
} | |
} | |
// ... lines 109 - 110 | |
} | |
// ... lines 112 - 120 | |
} |
¡Hagámoslo! Desplázate hasta aquí y borra el ID 22. Observa. Bum. La fila desaparece, recibimos la notificación de tostado y la página no se mueve.
Vale, los últimos días han sido... vaya. Mañana nos lo tomaremos con más calma y aprenderemos otra forma de utilizar Turbo Streams. ¡Hasta entonces!
The deeper we go, the more confusig turbo-frames are...