Buy Access to Course
06.

Tailwind CSS

|

Share this awesome video!

|

Me encanta utilizar Tailwind para CSS. Si nunca lo has usado antes, o quizá sólo hayas oído hablar de él, puede que... lo odies al principio. Esto se debe a que utilizas clases dentro de HTML para definirlo todo. Y así tu HTML puede acabar pareciendo, bueno, un poco loco. Pero dale una oportunidad. Yo me he enamorado de él. Y, en lugar de parecerme feo, me parece descriptivo.

Tailwind requiere construcción

Tailwind no es tu tradicional monstruo CSS en el que descargas un archivo CSS gigante y lo incluyes. En su lugar, Tailwind tiene un binario que analiza todas tus plantillas, encuentra las clases que utilizas y vuelca un CSS final que contiene sólo esas clases. Así mantiene tu CSS final lo más pequeño posible.

¡Pero para hacer esto, duh duh duh! Tailwind requiere un paso de compilación. Y no pasa nada. Que no tengamos un paso de compilación para todo nuestro sistema JavaScript no significa que no podamos optar por uno pequeño para un propósito específico.

Instalación de symfonycasts/tailwind-bundle

Existe un bundle superfácil para ayudarnos a hacer esto con AssetMapper. Se llamasymfonycasts/tailwind-bundle. Oye, ¡he oído hablar de ellos!

Baja aquí, ve a la documentación... y copiaré la línea composer require. Gira y ejecútalo:

composer require symfonycasts/tailwind-bundle

Este bundle nos ayudará a ejecutar el comando de compilación en segundo plano y a servir el archivo final. Lo apuntamos a un archivo CSS real, y luego colará el contenido dinámico. Ya lo verás.

Mientras estamos aquí, Ejecuta::

php bin/console debug:config symfonycasts_tailwind

para ver la configuración por defecto del bundle. Por defecto, el archivo que "construye" es assets/styles/app.css... ¡lo cual es genial porque ya tenemos un archivo assets/styles/app.css!

Para poner las cosas en su sitio, ejecuta un comando del bundle:

php bin/console tailwind:init

Esto descarga el binario de Tailwind en segundo plano, lo cual es genial. Ese binario es independiente y no requiere Node. Simplemente funciona. El comando también hizo otras dos cosas. Primero: añadió las tres líneas necesarias dentro de app.css:

8 lines | assets/styles/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;
// ... lines 4 - 8

Cuando se construya este archivo, se sustituirán por el CSS real que necesitamos. En segundo lugar, creó un archivo tailwind.config.js:

12 lines | tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./assets/**/*.js",
"./templates/**/*.html.twig",
],
theme: {
extend: {},
},
plugins: [],
}

Esto le dice a Tailwind dónde buscar todas las clases que utilizaremos. Esto encontrará cualquier clase en nuestros archivos JavaScript o en nuestras plantillas.

Para ejecutar Tailwind, ejecuta:

php bin/console tailwind:build -w

Para ver. Eso construye... y luego se queda colgado, esperando futuros cambios.

Y... ¿qué ha hecho eso? Recuerda: ya estamos incluyendo app.css en nuestra página. Cuando actualizamos, ¡woh! ¡Se ve un poco diferente! La razón es que, si ves el código fuente de la página y haces clic para abrir el archivo app.css, ¡está lleno de código Tailwind! ¡Nuestro archivoapp.css ya no es exactamente este archivo fuente! Entre bastidores, el binario de Tailwind analiza nuestras plantillas y vuelca una versión final de este archivo, que luego devuelve. Este archivo ya contiene un montón de código porque llené las plantillas CRUD con clases Tailwind antes de empezar el tutorial.

Utilizar Tailwind

Pero veamos esto en acción de verdad. Si refrescamos la página, este es mi h1. Es pequeño y triste. Abre templates/main/homepage.html.twig. Enh1, añade class="text-3xl":

8 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<h1 class="text-3xl">Space Inviters: Plan your voyage and come in peace!</h1>
{% endblock %}

Ahora, actualiza. ¡Funciona! Si ese text-3xl no estaba antes en el archivo app.css, Tailwind acaba de añadirlo porque se está ejecutando en segundo plano.

Pegar el diseño

¡Así que Tailwind funciona! Para celebrarlo, vamos a pegar un diseño adecuado. Si has descargado el código del curso, deberías tener un directorio tutorial/ con un par de archivos. Mueve base.html.twig a plantillas:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Space Inviters!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
{% block stylesheets %}
{% endblock %}
{% block javascripts %}
{{ importmap('app') }}
{% endblock %}
</head>
<body class="bg-black text-white font-mono">
<div class="container mx-auto min-h-screen flex flex-col">
<header class="my-8 px-4">
<nav class="flex items-center justify-between mb-4">
<div class="flex items-center">
<a href="{{ path('app_homepage') }}">
<img src="{{ asset('images/logo.png') }}" width="50" alt="Space Inviters Logo" >
</a>
<a href="{{ path('app_homepage') }}" class="text-xl ml-3">Space Inviters</a>
<a href="{{ path('app_voyage_index') }}" class="ml-6 hover:text-gray-400">Voyages</a>
<a href="{{ path('app_planet_index') }}" class="ml-4 hover:text-gray-400">Planets</a>
</div>
<div
class="hidden md:flex pr-10 items-center space-x-2 border-2 border-gray-900 rounded-lg p-2 bg-gray-800 text-white cursor-pointer"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/><path d="M21 21l-6 -6"/></svg>
<span class="pl-2 pr-10 text-gray-500">Search Cmd+K</span>
</div>
</nav>
</header>
<!-- Make sure the main tag takes up the remaining height -->
<main class="flex-grow">{% block body %}{% endblock %}</main>
<!-- Footer -->
<footer class="py-4 mt-6 bg-gray-800 text-center">
<div class="text-sm">
With <svg class="inline-block w-4 h-4 text-red-600 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10 3.22l-.61-.6a5.5 5.5 0 00-7.78 7.78l7.39 7.4 7.39-7.4a5.5 5.5 0 00-7.78-7.78l-.61.61z"/></svg> from Symfonycasts.
</div>
</footer>
</div>
</body>
</html>

Y estos otros dos al directorio main/:

{% extends 'base.html.twig' %}
{% block title %}Space Inviters!{% endblock %}
{% block body %}
<div class="flex">
<aside class="hidden md:block md:w-64 bg-gray-900 px-2 py-6">
<h2 class="text-xl text-white font-semibold mb-6 px-2">Planets</h2>
<div>
{{ include('main/_planet_list.html.twig') }}
</div>
</aside>
<section class="flex-1 ml-10">
<form
method="GET"
action="{{ path('app_homepage') }}"
class="mb-6 flex justify-between"
>
<input
type="search"
name="query"
value="{{ app.request.query.get('query') }}"
aria-label="Search voyages"
placeholder="Search voyages"
class="w-1/3 px-4 py-2 rounded bg-gray-800 text-white placeholder-gray-400"
>
<div class="whitespace-nowrap m-2 mr-4">{{ voyages|length }} results</div>
</form>
<div class="bg-gray-800 p-4 rounded">
<table class="w-full text-white">
<thead>
<tr>
<th class="text-left py-2">Purpose</th>
<th class="text-left py-2 pr-4">Planet</th>
<th class="text-left py-2">Departing</th>
</tr>
</thead>
<tbody>
{% for voyage in voyages %}
<tr class="border-b border-gray-700 {% if loop.index is odd %} bg-gray-800 {% else %} bg-gray-700 {% endif %}">
<td class="p-4">{{ voyage.purpose }}</td>
<td class="px-2 whitespace-nowrap">
<img
src="{{ asset('images/'~voyage.planet.imageFilename) }}"
alt="Image of {{ voyage.planet.name }}"
class="inline-block w-8 h-8 rounded-full bg-gray-600 ml-2"
>
</td>
<td class="px-2 whitespace-nowrap">{{ voyage.leaveAt|date('Y-m-d') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="flex items-center mt-6 space-x-4">
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Previous</a>
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Next</a>
</div>
</section>
</div>
{% endblock %}

<ul>
{% for planet in planets %}
<li class="mb-4 group">
<a href="{{ path('app_planet_show', {
'id': planet.id,
}) }}" class="block transform transition duration-300 ease-in-out hover:translate-x-1 hover:bg-gray-700 rounded">
<div class="flex justify-between items-start p-2">
<div class="pr-3">
<span class="text-white">{{ planet.name }}</span>
<span class="block text-gray-400 text-sm">{{ planet.lightYearsFromEarth|round|number_format }} light years</span>
</div>
<img
class="flex-shrink-0 w-8 h-8 bg-gray-600 rounded-full group-hover:bg-gray-500 transition duration-300 ease-in-out"
src="#"
alt="Image of {{ planet.name }}"
>
</div>
</a>
</li>
{% endfor %}
</ul>

Actualizar. Huh, no hay diferencia. Eso es porque, al menos en un Mac, como moví y sobrescribí esos archivos, Twig no se dio cuenta de que estaban actualizados... por lo que su caché está desactualizada.

Abre una nueva pestaña del terminal y ejecuta:

php bin/console cache:clear

Entonces... ¡guau! ¡Bienvenido a Space Inviters! ¡Con estilo y listo para empezar! Pero las nuevas plantillas no tienen nada de especial. Tenemos una lista de viajes... pero todo es aburrido, código Twig normal con clases Tailwind.

Referenciar activos dinámicamente

Tenemos algunas imágenes de planetas rotas. Para arreglarlas, entra en el directoriotutorial/assets/... y mueve todos esos planetas a assets/images/. Elimina la carpeta tutorial/.

Esa etiqueta img rota proviene de la parcial _planet_list.html.twig. Aquí la tienes:

22 lines | templates/main/_planet_list.html.twig
<ul>
{% for planet in planets %}
<li class="mb-4 group">
<a href="{{ path('app_planet_show', {
'id': planet.id,
}) }}" class="block transform transition duration-300 ease-in-out hover:translate-x-1 hover:bg-gray-700 rounded">
<div class="flex justify-between items-start p-2">
// ... lines 8 - 11
<img
// ... line 13
src="#"
// ... line 15
>
</div>
</a>
</li>
{% endfor %}
</ul>

¡Lo dejé para que lo termináramos! ¡Qué amable por mi parte! Sabemos que podemos hacer {{ assets() }}y luego algo como images/planets-1.png. Eso funcionaría. Pero esta vez, la parteplanet-1.png es una propiedad dinámica de la entidad Planet. Así que, en vez de eso, di ~ y luego planet.imageFilename:

22 lines | templates/main/_planet_list.html.twig
<ul>
{% for planet in planets %}
<li class="mb-4 group">
<a href="{{ path('app_planet_show', {
'id': planet.id,
}) }}" class="block transform transition duration-300 ease-in-out hover:translate-x-1 hover:bg-gray-700 rounded">
<div class="flex justify-between items-start p-2">
// ... lines 8 - 11
<img
// ... line 13
src="{{ asset('images/'~planet.imageFilename) }}"
// ... line 15
>
</div>
</a>
</li>
{% endfor %}
</ul>

Y... ¡bonito! Sí, ya sé que la Tierra y Saturno no tienen ese aspecto -tengo algo de aleatoriedad en mis instalaciones-, ¡pero es divertido verlos!

Uso de KnpTimeBundle

Ya que el día 6 es el día de "hacer que todo parezca increíble", vamos a hacer dos cosas más. Para empezar, no me encanta esta fecha. Es aburrida Quiero una fecha con un aspecto genial.

Así que instala uno de mis bundles favoritos:

composer require knplabs/knp-time-bundle

Esto nos proporciona un práctico filtro ago. Así que en cuanto esto termine, gira y abrehomepage.html.twig. Busca leaveAt y ya está. Sustituye ese filtro date por ago:

63 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<div class="flex">
// ... lines 7 - 13
<section class="flex-1 ml-10">
// ... lines 15 - 29
<div class="bg-gray-800 p-4 rounded">
<table class="w-full text-white">
// ... lines 32 - 38
<tbody>
{% for voyage in voyages %}
<tr class="border-b border-gray-700 {% if loop.index is odd %} bg-gray-800 {% else %} bg-gray-700 {% endif %}">
// ... lines 42 - 49
<td class="px-2 whitespace-nowrap">{{ voyage.leaveAt|ago }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
// ... lines 56 - 59
</section>
</div>
{% endblock %}

Y... ¡mucho más chulo!

¿Qué más? Echa un vistazo a las áreas CRUD. Éstas se generaron mediante MakerBundle... pero... Las precargué con clases de Tailwind para que tuvieran buen aspecto. Vaya, hay mucho que celebrar ahora mismo. No me quejo.

Pero... si vas a un formulario, ¡se ve fatal! ¿Por qué? El marcado del formulario proviene del tema de formularios de Symfony... que genera los campos sin clases Tailwind.

Ejemplos de Flowbite para Tailwind

Entonces, ¿qué hacemos? ¿Tenemos que crear un tema de formulario? Afortunadamente, no. Una de las cosas buenas de Tailwind es que hay todo un ecosistema creado a su alrededor. Por ejemplo, Flowbite es un sitio con una mezcla de ejemplos de código abierto y funciones profesionales de pago. En la parte de código abierto puedes encontrar, por ejemplo, una página de "Alertas" con diferentes marcas para crear alertas de gran aspecto. Y no necesitas instalar nada con Flowbite. Todas estas clases son nativas de Tailwind. Puedes copiar este marcado en tu proyecto y actualizarlo. Nada especial. Y me encanta.

Esto también tiene una sección de formularios donde muestra cómo podemos hacer que los formularios tengan un aspecto realmente bonito. En teoría, si pudiéramos hacer que nuestros formularios salieran así, tendrían un aspecto estupendo.

Añadir un tema de formulario Tailwind

Y afortunadamente, hay un bundle que puede ayudarnos. Se llamatales-from-a-dev/flowbite-bundle. Haz clic en "Instalación" y copia la línea composer require. Luego ejecútalo:

composer require tales-from-a-dev/flowbite-bundle

¡Hoy nos van a instalar un montón de cosas! Nos pregunta si queremos instalar la receta contrib. Digamos que sí, permanentemente. ¡Genial!

Volviendo a las instrucciones de instalación, no necesitamos registrar el bundle -eso ocurre automáticamente-, pero sí necesitamos copiar esta línea para el archivo de configuración de tailwind.

Abre tailwind.config.js, y pega esto:

13 lines | tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
// ... lines 4 - 5
"./vendor/tales-from-a-dev/flowbite-bundle/templates/**/*.html.twig",
],
// ... lines 8 - 11
}

Este bundle viene con su propia plantilla de tema de formulario con sus propias clases Tailwind, así que queremos asegurarnos de que Tailwind las ve y genera el CSS para ellas.

El último paso en los documentos es decirle a nuestro sistema que utilice este tema de formulario por defecto. Esto ocurre en config/packages/twig.yaml. Lo pegaré... y luego arreglaré la sangría:

8 lines | config/packages/twig.yaml
twig:
// ... line 2
form_themes: ['@Flowbite/form/default.html.twig']
// ... lines 4 - 8

Ya está. Vuelve atrás, actualiza y ¡eureka! En poco más de 10 minutos, instalamos Tailwind y empezamos a utilizarlo en todas partes.

Mañana volveremos a JavaScript y aprovecharemos Stimulus para escribir código JavaScript fiable que nos encante.