Buy Access to Course
26.

Componente Twig 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

Hoy es un buen día. Hoy vamos a combinar nuestro sistema modal con componentes Twig para conseguir un objetivo Quiero poder añadir rápidamente un modal en cualquier parte de nuestra app.

Crear el componente modal

Comienza en base.html.twig. En la parte inferior, copia el marcado modal. Ya ves... es bastante: no es algo que queramos copiar y pegar en otro sitio:

97 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"
>
<dialog
class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%] animate-fade-in backdrop:bg-slate-600 backdrop:opacity-80"
data-modal-target="dialog"
data-action="close->modal#close click->modal#clickOutside"
>
<div class="flex grow p-5">
<div class="grow overflow-auto p-1">
<turbo-frame
id="modal"
data-modal-target="dynamicContent"
data-action="turbo:before-fetch-request->modal#showLoading"
class="aria-busy:opacity-50 transition-opacity"
>
{{ include('_frameSuccessStreams.html.twig', { frame: 'modal' }) }}
</turbo-frame>
</div>
</div>
</dialog>
<template data-modal-target="loadingTemplate">
<div class="bg-space-pattern bg-cover rounded-lg p-8">
<div class="space-y-2">
<div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded animate-pulse"></div>
<div class="h-4"></div>
<div class="h-4 bg-gray-700 rounded animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div>
<div class="h-4"></div>
<div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div>
</div>
</div>
</template>
</div>
// ... lines 91 - 94
</body>
</html>

Cópialo y luego bórralo. Vamos a crear un componente Modal, esta vez a mano. Crea un nuevo archivo en templates/components/ llamado Modal.html.twig, y pégalo:

<div
data-controller="modal"
data-action="turbo:before-cache@window->modal#close"
>
<dialog
class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%] animate-fade-in backdrop:bg-slate-600 backdrop:opacity-80"
data-modal-target="dialog"
data-action="close->modal#close click->modal#clickOutside"
>
<div class="flex grow p-5">
<div class="grow overflow-auto p-1">
<turbo-frame
id="modal"
data-modal-target="dynamicContent"
data-action="turbo:before-fetch-request->modal#showLoading"
class="aria-busy:opacity-50 transition-opacity"
>
{{ include('_frameSuccessStreams.html.twig', { frame: 'modal' }) }}
</turbo-frame>
</div>
</div>
</dialog>
<template data-modal-target="loadingTemplate">
<div class="bg-space-pattern bg-cover rounded-lg p-8">
<div class="space-y-2">
<div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded animate-pulse"></div>
<div class="h-4"></div>
<div class="h-4 bg-gray-700 rounded animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div>
<div class="h-4"></div>
<div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div>
</div>
</div>
</template>
</div>

Como dije con el Button, no necesitamos una clase PHP para un componente. Como no tenemos ninguna, lo llamaremos "componente anónimo".

Encima, renderiza attributes... luego añade .defaults() para que podamos mover estos dos atributos a eso. Pega... luego cada uno de ellos necesita un cambio de imagen para encajar como claves y valores Twig en lugar de atributos HTML:

42 lines | templates/components/Modal.html.twig
<div
{{ attributes.defaults({
'data-controller': 'modal',
'data-action': 'turbo:before-cache@window->modal#close',
}) }}
>
// ... lines 7 - 40
</div>

¡Me gusta! Sobre base.html.twig, renderiza esto con <twig:Modal>. ¡Qué fácil!

Añadir bloques al componente

Sin embargo, fíjate bien en Modal.html.twig: hay algunas cosas que no deberían estar aquí. Por ejemplo, ¡el <turbo-frame>! No todos los modales necesitan un marco. Muchas veces, renderizaremos un modal con contenido simple y codificado dentro.

Copia esto y sustitúyelo, por supuesto, por {% block content %} y {% endblock %}:

35 lines | templates/components/Modal.html.twig
<div
// ... lines 2 - 5
>
<dialog
// ... lines 8 - 10
>
<div class="flex grow p-5">
<div class="grow overflow-auto p-1">
{% block content %}{% endblock %}
</div>
</div>
</dialog>
// ... lines 18 - 33
</div>

En base.html.twig, pega el marco... y añade una etiqueta de cierre:

67 lines | templates/base.html.twig
<!DOCTYPE html>
<html>
// ... lines 3 - 15
<body class="bg-black text-white font-mono">
// ... lines 17 - 54
<twig:Modal>
<turbo-frame
id="modal"
data-modal-target="dynamicContent"
data-action="turbo:before-fetch-request->modal#showLoading"
class="aria-busy:opacity-50 transition-opacity"
>
{{ include('_frameSuccessStreams.html.twig', { frame: 'modal' }) }}
</turbo-frame>
</twig:Modal>
</body>
</html>

¡Sigamos! ¿La plantilla de carga de aquí abajo? Sí, también es algo específico de este modal. Si nuestro modal es un mensaje hardcoded, ¡no necesitará esto en absoluto!

Copia el div de carga , bórralo, luego alrededor del <template> añade: ifblock('loading_template'):

25 lines | templates/components/Modal.html.twig
<div
// ... lines 2 - 5
>
// ... lines 7 - 18
{% if block('loading_template') %}
<template data-modal-target="loadingTemplate">
{% block loading_template %}{% endblock %}
</template>
{% endif %}
</div>

Así que si pasamos el bloque, renderízalo dentro de <template>.

De vuelta en base.html.twig, en cualquier lugar, define ese bloque. Pero en lugar de la etiqueta normal{% block %} -que funcionaría-, cuando estás dentro de un componente Twig, puedes utilizar una sintaxis especial <twig:block> con name="loading_template". Luego, pega:

82 lines | templates/base.html.twig
<!DOCTYPE html>
<html>
// ... lines 3 - 15
<body class="bg-black text-white font-mono">
// ... lines 17 - 54
<twig:Modal>
<turbo-frame
// ... lines 57 - 62
</turbo-frame>
<twig:block name="loading_template">
<div class="bg-space-pattern bg-cover rounded-lg p-8">
<div class="space-y-2">
<div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded animate-pulse"></div>
<div class="h-4"></div>
<div class="h-4 bg-gray-700 rounded animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div>
<div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div>
<div class="h-4"></div>
<div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div>
</div>
</div>
</twig:block>
</twig:Modal>
</body>
</html>

Acabamos de mover un montón de cosas. Y aún así... ¡el modal existente sigue funcionando! Y ahora, tenemos un componente modal más esbelto y eficaz que podemos reutilizar en otros lugares.

Eliminar el modal con contenido personalizado

Hagamos exactamente eso. Quiero añadir un enlace de borrado en cada fila que, al hacer clic, abra un modal con una confirmación. Abre _row.html.twig. Copia el enlace de edición, pégalo y llámalo eliminar:

19 lines | templates/voyage/_row.html.twig
<tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}">
// ... lines 2 - 4
<td class="px-6 py-4 whitespace-nowrap">
// ... lines 6 - 11
<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>

Para que esto funcione, una opción es crear una nueva página independiente de confirmación de eliminación, apuntar a ella y... ¡listo! El data-turbo-frame="modal"cargaría esa página en el modal.

Pero como ya hemos hecho eso antes, vamos a probar algo diferente. Elimina el href, cámbialo por un button, elimina el atributo data-turbo-frame... y cambia los colores amarillos por rojos:

17 lines | templates/voyage/_row.html.twig
<tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}">
// ... lines 2 - 4
<td class="px-6 py-4 whitespace-nowrap">
// ... lines 6 - 11
<button
class="ml-4 text-red-400 hover:text-red-600"
>delete</button>
</td>
</tr>

Vamos a comprobar el aspecto. ¡Qué bonito!

De vuelta, pegaré un modal:

24 lines | templates/voyage/_row.html.twig
<tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}">
// ... lines 2 - 4
<td class="px-6 py-4 whitespace-nowrap">
// ... lines 6 - 14
<twig:Modal>
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">
Delete this thrilling voyage???
</h3>
</twig:Modal>
</td>
</tr>

Aquí no hay nada especial. La gran diferencia es que, en lugar de un <turbo-frame>, el contenido que necesitamos está justo aquí. Cuando actualizamos, cada fila tiene ahora un cuadro de diálogo de borrado en su interior. ¡Pero no pasa nada! No está abierto, así que es invisible.

Abrir el modal

Ahora viene la parte complicada. Cuando hagamos clic en este botón, tendremos que abrir este modal. Para que esto ocurra, necesitamos que el botón viva físicamente dentro del elementodata-controller="modal" para que pueda llamar a la acción open en el controlador modal Stimulus.

Para permitirlo, dentro de la plantilla modal, añade un nuevo bloque llamado trigger,endblock:

27 lines | templates/components/Modal.html.twig
<div
// ... lines 2 - 5
>
{% block trigger %}{% endblock %}
// ... lines 8 - 25
</div>

Ahora, si tienes un botón que activa la apertura del modal, ¡puedes ponerlo aquí! En _row.html.twig, copia el botón, quítalo, pon<twig:block name="trigger"> y pégalo.

Y como estamos dentro del modal, añade data-action="modal#open":

29 lines | templates/voyage/_row.html.twig
<tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}">
// ... lines 2 - 4
<td class="px-6 py-4 whitespace-nowrap flex">
// ... lines 6 - 12
<twig:Modal>
<twig:block name="trigger">
<button
class="ml-4 text-red-400 hover:text-red-600"
data-action="modal#open"
>delete</button>
</twig:block>
// ... lines 20 - 25
</twig:Modal>
</td>
</tr>

¡Probemos esto! ¡Qué emoción! ¡Actualiza! El estilo se ha vuelto raro. Antes teníamos tres etiquetas a, que son elementos en línea. Ahora tenemos dos elementos en línea y un elemento de bloque. Así que es algo que cambia ligeramente, pero tiene fácil arreglo. Arriba, en <td>, añade una clase flex:

29 lines | templates/voyage/_row.html.twig
<tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}">
// ... lines 2 - 4
<td class="px-6 py-4 whitespace-nowrap flex">
// ... lines 6 - 26
</td>
</tr>

Tamaño Modal Condicional y la Etiqueta props

Y ahora... mucho mejor. Y lo que es más importante, cuando pulsamos Eliminar, ¡el modal se abre! Sin embargo, ya me conoces, quiero que esto sea perfecto. Y no estoy contento con el tamaño que tiene. Cuando abro el formulario de edición, tiene sentido que ocupe la mitad del ancho de la pantalla. Pero cuando abro el de borrar, deberíamos dejar que se redujera al tamaño del contenido que hay dentro.

Para ello, en este caso, quiero pasarle una nueva bandera llamada allowSmallWidthestablecida en true:

29 lines | templates/voyage/_row.html.twig
<tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}">
// ... lines 2 - 4
<td class="px-6 py-4 whitespace-nowrap flex">
// ... lines 6 - 12
<twig:Modal :allowSmallWidth="true">
// ... lines 14 - 25
</twig:Modal>
</td>
</tr>

He añadido este : porque, si paso allowSmallWidth="true", pasará la cadena true. Al añadir dos puntos, esto se convierte en código Twig, por lo que pasará el booleano true. Ambos funcionarían... pero me gusta ser más estricto.

Con el Button, aprendimos que si quieres que esto se convierta en una variable en lugar de un atributo, puedes añadir una propiedad pública con ese mismo nombre. Y podríamos crear un nuevo archivo Modal.php.

Pero hay otra forma de convertir un atributo en una variable cuando se utiliza un componente anónimo. En la parte superior de Modal.html.twig, añade una etiqueta propsque es especial para los componentes Twig. Añade allowSmallWidth y ponla por defecto enfalse:

28 lines | templates/components/Modal.html.twig
{% props allowSmallWidth=false %}
// ... lines 2 - 28

Genial, ¿eh? A continuación, queremos hacer que este ancho mínimo sea condicional. Digamos{{ allowSmallWidth }} - si es cierto, no renderiza nada, sino renderiza elmd:min-w-[50%]:

28 lines | templates/components/Modal.html.twig
{% props allowSmallWidth=false %}
<div
// ... lines 3 - 6
>
// ... lines 8 - 9
<dialog
class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] {{ allowSmallWidth ? '' : 'md:min-w-[50%] ' }}animate-fade-in backdrop:bg-slate-600 backdrop:opacity-80"
// ... lines 12 - 13
>
// ... lines 15 - 19
</dialog>
// ... lines 21 - 26
</div>

De vuelta a la página, el enlace de edición sigue abriéndose a media anchura... pero el enlace de borrado, ¡ah, es bonito y pequeño! ¡Ahora se merece un contenido de verdad! En _row.html.twig, después de <h3>, añadiré algo de estilo... luego quiero un botón de cancelar que cierre el modal. Para eso, podemos ir a la vieja escuela. Añade un <form method="dialog">, y dentro un <twig:Button> que diga Cancelar. Y quiero que el botón parezca un enlace, así que añade variant="link":

36 lines | templates/voyage/_row.html.twig
<tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}">
// ... lines 2 - 4
<td class="px-6 py-4 whitespace-nowrap flex">
// ... lines 6 - 12
<twig:Modal :allowSmallWidth="true">
// ... lines 14 - 26
<div class="flex justify-between">
<form method="dialog">
<twig:Button variant="link">Cancel</twig:Button>
</form>
// ... line 31
</div>
</twig:Modal>
</td>
</tr>

Eso aún no existe, así que en la clase Button, añádelo: variant y sólo necesita text-white:

24 lines | src/Twig/Components/Button.php
// ... lines 1 - 7
class Button
{
// ... lines 10 - 12
public function getVariantClasses(): string
{
return match ($this->variant) {
// ... lines 16 - 18
'link' => 'text-white',
// ... line 20
};
}
}

Después del formulario, para renderizar el botón de eliminar, incluye voyage/_delete_form.html.twig:

36 lines | templates/voyage/_row.html.twig
<tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}">
// ... lines 2 - 4
<td class="px-6 py-4 whitespace-nowrap flex">
// ... lines 6 - 12
<twig:Modal :allowSmallWidth="true">
// ... lines 14 - 26
<div class="flex justify-between">
<form method="dialog">
<twig:Button variant="link">Cancel</twig:Button>
</form>
{{ include('voyage/_delete_form.html.twig') }}
</div>
</twig:Modal>
</td>
</tr>

Ah, y esa plantilla tiene incorporado confirm. Elimínalo porque ahora tenemos algo mucho mejor.

¡Momento de la verdad! Actualiza y borra. ¡Queda genial! Cancelar cierra el modal... y borrar funciona. Y no debería sorprender que funcione. El formulario de borrado no está dentro de un <turbo-frame>. Así que cuando hacemos clic en borrar, se activa un envío de formulario normal que redirige y provoca una navegación normal por toda la página.

Ocultar las opciones de búsqueda en un modal

Vale, sé que esto ya es un día completo, pero quiero utilizar el modal en un punto más. Y es un caso de uso genial.

En la página de inicio, en mi código PHP y Symfony, no lo mostraré, pero ya he añadido lógica para filtrar esta lista por los planetas. Sólo que no añadí ninguna casilla de verificación de planetas a la página porque... en realidad no tenemos espacio para ellas.

Así que ésta es mi idea: añade un enlace aquí que abra un modal que contenga las opciones de filtrado adicionales.

Abre main/homepage.html.twig y busca esa entrada. Empieza añadiendo un<div class="w-1/3 flex">... añade el cierre al otro lado de la entrada... luego elimina w-1/3 de la entrada. Estamos haciendo espacio para ese enlace:

149 lines | templates/main/homepage.html.twig
// ... lines 1 - 27
{% block body %}
<div class="flex">
// ... lines 30 - 36
<section class="flex-1 ml-10">
<form
method="GET"
action="{{ path('app_homepage') }}"
class="mb-6 flex justify-between"
data-controller="autosubmit"
data-turbo-frame="voyage-list"
>
<div class="w-1/3 flex">
<input
type="search"
name="query"
value="{{ app.request.query.get('query') }}"
aria-label="Search voyages"
placeholder="Search voyages"
class="px-4 py-2 rounded bg-gray-800 text-white placeholder-gray-400"
data-action="autosubmit#debouncedSubmit"
autofocus
>
</div>
// ... lines 57 - 59
</form>
// ... lines 61 - 145
</section>
</div>
{% endblock %}

Pero pegaré un modal completo:

169 lines | templates/main/homepage.html.twig
// ... lines 1 - 27
{% block body %}
<div class="flex">
// ... lines 30 - 36
<section class="flex-1 ml-10">
<form
method="GET"
action="{{ path('app_homepage') }}"
class="mb-6 flex justify-between"
data-controller="autosubmit"
data-turbo-frame="voyage-list"
>
<div class="w-1/3 flex">
<input
type="search"
name="query"
value="{{ app.request.query.get('query') }}"
aria-label="Search voyages"
placeholder="Search voyages"
class="px-4 py-2 rounded bg-gray-800 text-white placeholder-gray-400"
data-action="autosubmit#debouncedSubmit"
autofocus
>
<twig:Modal>
<twig:block name="trigger">
<twig:Button
variant="link"
type="button"
data-action="modal#open"
>Options</twig:Button>
</twig:block>
<h3 class="text-white text-lg font-semibold mb-2">Search Options</h3>
<hr class="mb-4">
<div class="flex justify-end">
<twig:Button
variant="success"
data-action="modal#close"
>See Results</twig:Button>
</div>
</twig:Modal>
</div>
// ... lines 77 - 79
</form>
// ... lines 81 - 165
</section>
</div>
{% endblock %}

Esto será invisible excepto por el desencadenante. Así que básicamente acabamos de añadir un botón que dice "opciones". Pero ya está configurado para abrir el modal. Dentro, para empezar, tenemos un h3 y un <twig:Button> que cierra el modal.

Añadir un botón de cierre del modal

Pero el resultado cuando hago clic en opciones... ¡es bonito! Aunque, necesita un botón de cierre en la parte superior derecha. Podríamos añadirlo sólo a este modal... pero estaría bien que fuera una opción del componente modal.

¡Hagámoslo! En Modal.html.twig, añade una prop más llamada closeButtonpor defecto a false:

37 lines | templates/components/Modal.html.twig
{% props allowSmallWidth=false, closeButton=false %}
// ... lines 2 - 37

Si es así, al final de dialog, pegaré un botón de cerrar:

37 lines | templates/components/Modal.html.twig
{% props allowSmallWidth=false, closeButton=false %}
<div
// ... lines 3 - 6
>
// ... lines 8 - 9
<dialog
// ... lines 11 - 13
>
// ... lines 15 - 19
{% if closeButton %}
<button
class="absolute right-4 top-3 text-white flex items-center opacity-70 transition-opacity hover:opacity-100"
data-action="modal#close"
type="button"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>
</button>
{% endif %}
</dialog>
// ... lines 30 - 35
</div>

De nuevo, aquí no hay nada especial: algo de estilo absoluto, un icono... y la parte importante: llama a modal#close.

En homepage.html.twig encuentra ese modal y añade closeButton="true"... pero con el : como la última vez:

169 lines | templates/main/homepage.html.twig
// ... lines 1 - 27
{% block body %}
<div class="flex">
// ... lines 30 - 36
<section class="flex-1 ml-10">
<form
// ... lines 39 - 43
>
<div class="w-1/3 flex">
// ... lines 46 - 56
<twig:Modal :closeButton="true">
// ... lines 58 - 74
</twig:Modal>
</div>
// ... lines 77 - 79
</form>
// ... lines 81 - 165
</section>
</div>
{% endblock %}

¡Vamos a verlo! ¡Me encanta!

Por último, vamos a escarchar este pastel. Cerca de la parte inferior del contenido, pegaré las casillas de verificación del planeta:

184 lines | templates/main/homepage.html.twig
// ... lines 1 - 27
{% block body %}
<div class="flex">
// ... lines 30 - 36
<section class="flex-1 ml-10">
<form
// ... lines 39 - 43
>
<div class="w-1/3 flex">
// ... lines 46 - 56
<twig:Modal :closeButton="true">
// ... lines 58 - 65
<h3 class="text-white text-lg font-semibold mb-2">Search Options</h3>
<hr class="mb-4">
<h4 class="text-white text-sm font-semibold mb-2">
Planets
</h4>
{% for planet in planets %}
<div class="flex items-center mb-4">
<input
type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
name="planets[]"
value="{{ planet.id }}"
id="planet-search-{{ planet.id }}"
{{ planet.id in searchPlanets ? 'checked' : '' }}
>
<label for="planet-search-{{ planet.id }}" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ planet.name }}</label>
</div>
{% endfor %}
// ... lines 84 - 89
</twig:Modal>
</div>
// ... lines 92 - 94
</form>
// ... lines 96 - 180
</section>
</div>
{% endblock %}

¡Esto es más código aburrido! Mi controlador Symfony ya está configurado para leer el parámetro planets y filtrar la consulta.

Prueba final. Ábrelo. ¡Precioso! Ahora observa: haz clic en algunos. Cuando pulse "Ver resultados", la tabla debería actualizarse. Bum. ¡Se ha actualizado!

Pero lo mejor es... ¡cómo ha funcionado! Piénsalo: Pulso este botón... y la tabla se recarga. Eso significa que el formulario se está enviando. Pero... ¿qué lo ha provocado? Mira el botón: no hay código para enviar el formulario. Entonces, ¿qué está pasando?

Recuerda: este button, las casillas de verificación del planeta y este modal viven físicamente dentro del elemento <form>. ¿Y qué ocurre cuando pulsas un botón que vive dentro de un formulario? ¡Envía el formulario! Ejecutamos el modal#close, pero también permitimos que el navegador realice el comportamiento por defecto: enviar el formulario. ¡Esto es tecnología alienígena antigua en acción!

En cuanto al botón de cierre, he sido un poco astuto. Cuando lo añadí, incluí untype="button". Eso le dice al navegador que no envíe ningún formulario que pueda estar dentro. Por eso, cuando hacemos clic en "X", no se actualiza nada. Pero cuando hacemos clic en "ver resultados", el formulario se envía.

¡Woh! ¡El mejor día de todos! Mañana veremos los componentes en vivo, en los que tomamos componentes Twig y dejamos que se reproduzcan en la página mediante Ajax cuando el usuario interactúa con ellos.