Skip to content

03 gifs app

Janio Isacura edited this page Oct 11, 2026 · 2 revisions

Parte 3: GIFs App

Esta página es una parte de la guía de estudio; allí están el mapa general y la ruta recomendada. La Parte 2, sobre las bases de Angular, está en 02-bases. Proyecto: 03-gifs-app/.

Es una aplicación Angular 22 creada con ng new, con SCSS para los estilos de cada componente y Tailwind CSS para las clases de utilidad. Tiene un dashboard con menú lateral y dos pantallas: Trending, que muestra una lista de imágenes de prueba, y Search, que todavía es una página vacía.


Módulo G1: Instalar Tailwind CSS en un proyecto con SCSS

El problema que resuelve

Quieres escribir class="text-3xl font-bold" en el HTML y que funcione, sin crear un archivo de estilos para cada detalle. Eso es Tailwind CSS: una colección de clases pequeñas (utility classes), cada una con un solo trabajo. El proyecto ya usa SCSS, y la pregunta es si pueden convivir. Sí pueden, con un cuidado.

Cómo se instala

Tailwind v4 se instala como un plugin de PostCSS (la herramienta que procesa el CSS después de escribirlo). Angular ya sabe leer su configuración.

  1. Instala los paquetes dentro de 03-gifs-app/:

    npm install tailwindcss @tailwindcss/postcss postcss
  2. Crea .postcssrc.json en la raíz del proyecto, para decirle a Angular que use el plugin:

    {
      "plugins": {
        "@tailwindcss/postcss": {}
      }
    }
  3. Crea src/tailwind.css con una sola línea:

    @import "tailwindcss";
  4. Cárgalo en angular.json, antes de tu SCSS:

    "styles": [
      "src/tailwind.css",
      "src/styles.scss"
    ]
  5. Reinicia ng serve y prueba una clase en app.html:

    <h1 class="text-3xl text-blue-500 font-bold">Welcome to the GIFs App</h1>

Por qué un archivo .css aparte

La primera idea es escribir @import "tailwindcss"; dentro de styles.scss. Funciona, pero Sass cree que ese @import es suyo y avisa que esa forma está obsoleta (deprecated) y desaparecerá en una versión futura. Para evitar el aviso, el import de Tailwind va en un archivo .css, que Sass no toca, y styles.scss queda para tus estilos propios.

angular.json
   │
   ├── src/tailwind.css ──► PostCSS + Tailwind ──┐
   │                                             ├──► styles.css final
   └── src/styles.scss ───► Sass ────────────────┘

SCSS y Tailwind juntos

  • Las clases de Tailwind se usan directo en el HTML. No hace falta tocar el .scss del componente.
  • Dentro del .scss de un componente, @apply y @theme no funcionan solos, porque ese archivo no sabe qué es Tailwind. Si los necesitas, empieza el archivo con @reference "tailwindcss";.
  • Después de cambiar .postcssrc.json o angular.json, reinicia ng serve: Angular los lee sólo al arrancar.

Errores comunes

  • Poner @import "tailwindcss" en styles.scss: da el aviso de Sass.
  • Olvidar agregar src/tailwind.css a styles en angular.json: las clases no hacen nada y no hay error.
  • No reiniciar ng serve tras crear .postcssrc.json.

Resumen

Tailwind v4 = tres paquetes + .postcssrc.json + un .css con su import, cargado desde angular.json. SCSS sigue igual para lo propio de cada componente.

Preguntas de entrevista

1. ¿Por qué @import "tailwindcss" va en un .css y no en el .scss?

Respuesta:

Porque Sass interpreta el @import antes de que Tailwind lo vea, y esa sintaxis está obsoleta en Sass (se eliminará en Dart Sass 3). En un .css, el import llega intacto a PostCSS, que es quien lo entiende. Por eso los dos archivos se listan por separado en styles de angular.json.

2. ¿Para qué sirve .postcssrc.json?

Respuesta:

Es donde Angular busca los plugins de PostCSS que debe aplicar al CSS. Ahí se registra @tailwindcss/postcss, que convierte @import "tailwindcss" en las clases de utilidad. Sin ese archivo, Tailwind no se ejecuta y las clases quedan sin efecto.

3. ¿Puedo usar @apply en el .scss de un componente?

Respuesta:

No directamente. Cada .scss de componente se compila por separado y no conoce Tailwind. Hay que declarar @reference "tailwindcss"; al inicio del archivo para que sepa qué clases existen, y aun así lo más simple es poner las clases en el HTML.

4. ¿Qué ventaja y qué costo tiene Tailwind frente a escribir SCSS?

Respuesta:

La ventaja es la velocidad y la consistencia: los valores (espacios, colores, tamaños) salen de una escala común y no inventas nombres de clases. El costo es que el HTML se llena de clases largas, y repetirlas en varios lugares obliga a extraer un componente. SCSS sigue siendo mejor para estilos muy específicos de un componente.


Módulo G2: Rutas con carga perezosa

El problema que resuelve

La app tiene tres pantallas: el dashboard, Trending y Search. Si el navegador descargara el código de las tres al abrir la página, el arranque sería más pesado de lo necesario, sobre todo cuando la app crezca. La carga perezosa (lazy loading) hace que cada pantalla se descargue sólo cuando el usuario entra a ella.

Cómo se ve en el proyecto

// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./gifs/pages/dashboard/dashboard-page.component'),
    children: [
      {
        path: 'trending',
        loadComponent: () => import('./gifs/pages/trending/trending-page.component'),
      },
      {
        path: 'search',
        loadComponent: () => import('./gifs/pages/search/search-page.component'),
      },
      { path: '**', redirectTo: 'trending' },
    ],
  },
  { path: '**', redirectTo: 'dashboard' },
]
  • loadComponent recibe una función que hace un import(): le dice a Angular "descarga este archivo sólo cuando se visite esta ruta".
  • children son las rutas que se dibujan dentro de la página padre. El dashboard tiene un <router-outlet /> en su plantilla, y ahí aparece Trending o Search. Por eso las URLs son /dashboard/trending y /dashboard/search.
  • ** significa "cualquier otra URL". Aquí no muestra una página: redirige.

Al compilar con ng build, Angular genera un archivo aparte por cada página (dashboard-page-component, trending-page-component, search-page-component) además del archivo principal.

Por qué export default

import() devuelve el archivo completo como un objeto. Con una exportación nombrada, hay que decirle a Angular cuál de sus partes es el componente:

// export class DashboardPageComponent {}
loadComponent: () =>
  import('./dashboard-page.component').then((m) => m.DashboardPageComponent)

Con export default, Angular ya sabe cuál tomar y basta con el import():

// export default class DashboardPageComponent {}
loadComponent: () => import('./dashboard-page.component')

Cuándo usar default y cuándo no

Componente Exportación
Página que se carga por una ruta (loadComponent) export default class
Componente que se usa en la plantilla de otro (imports) export class

Las páginas del proyecto (DashboardPageComponent, TrendingPageComponent, SearchPageComponent) llevan default. Los componentes de pieza (SideMenuComponent, GifListComponent, etc.) llevan nombre.

El orden importa: ** va al final

Angular recorre las rutas de arriba hacia abajo y se queda con la primera que coincide. ** coincide con todo, así que si va primero, tapa a las demás. Este proyecto tiene dos:

  • Dentro de dashboard: cualquier URL desconocida va a trending.
  • En la raíz: cualquier URL desconocida va a dashboard.

Así, /cualquier-cosa termina en /dashboard/trending.

Errores comunes

  • Poner ** antes que otras rutas: las demás nunca se alcanzan.
  • Usar export default en un componente que luego se mete en imports de otro: en este proyecto no lo reconoció, y se arregló volviendo a export class.
  • Olvidar el <router-outlet /> en la página padre: las rutas hijas cambian la URL, pero no se ve nada.

Resumen

loadComponent + import() = la pantalla se descarga al visitarla. children anida rutas dentro de un <router-outlet />. export default evita el .then(...), pero sólo para páginas de ruta.

Preguntas de entrevista

1. ¿Qué es la carga perezosa y qué gana una app con ella?

Respuesta:

Es descargar el código de una pantalla sólo cuando el usuario la visita, en vez de meter todo en el archivo inicial. Gana un arranque más rápido, porque el navegador baja menos código al principio. En rutas con componentes standalone se logra con loadComponent y un import() dinámico.

2. ¿Por qué con export default no hace falta .then(m => m.Componente)?

Respuesta:

Porque loadComponent sabe tomar la exportación default del archivo que devuelve import(). Con una exportación nombrada, el archivo trae varias cosas posibles y hay que indicar cuál es el componente.

3. ¿Todos los componentes deberían llevar export default?

Respuesta:

No. Sólo las páginas que se cargan por ruta. Los componentes que se usan en la plantilla de otro se importan por nombre en su imports, y ahí export class con nombre es lo esperado y lo que funciona en este proyecto.

4. ¿Qué diferencia hay entre una ruta con children y una ruta plana?

Respuesta:

Una ruta con children dibuja las hijas dentro de la plantilla del padre, en su <router-outlet />, así que el padre (aquí el dashboard con su menú) se queda en pantalla y sólo cambia la parte interior. Con rutas planas, cada ruta reemplaza la página completa.

5. ¿Por qué la ruta ** debe ir al final?

Respuesta:

Porque Angular usa la primera ruta que coincide, y ** coincide con cualquier URL. Al final actúa como "si nada más coincidió": en este proyecto, redirige a una página válida.


Módulo G3: Dashboard modular con componentes anidados

El problema que resuelve

El diseño del dashboard salió de una plantilla ya hecha en la web: un solo HTML largo con el encabezado, el menú y las opciones mezclados. Difícil de leer y de cambiar. La solución es partirlo en componentes pequeños, cada uno responsable de una sola parte de la pantalla.

Cómo queda armado

dashboard-page
 ├── gifs-side-menu
 │    ├── gifs-side-menu-header     logo, eslogan y perfil
 │    └── gifs-side-menu-options    enlaces del menú
 └── router-outlet                  aquí entra Trending o Search

La plantilla del dashboard queda corta:

<gifs-side-menu />

<div class="ml-55 px-4 flex-col flex-1 h-full text-slate-800">
  <router-outlet />
</div>

Cada componente declara lo que usa

Un componente standalone es su propio "módulo": para usar la etiqueta de otro componente en su plantilla, tiene que traer su clase en imports.

// dashboard-page.component.ts
imports: [RouterOutlet, SideMenuComponent]

// side-menu.component.ts
imports: [SideMenuHeaderComponent, SideMenuOptionsComponent]

Si la clase no está en imports, Angular no sabe qué es <gifs-side-menu /> y se queja de que no conoce esa etiqueta.

Detalles del diseño

  • Prefijo gifs- en los selectores (gifs-side-menu, gifs-side-menu-header): evita choques con etiquetas de HTML u otras librerías, y deja claro de dónde viene el componente.
  • Menú fijo y contenido con margen: el menú es fixed con ancho w-55 y el contenido lleva ml-55, el mismo valor. Si cambias uno, cambia el otro.
  • Un componente, una responsabilidad: el menú no sabe qué página se muestra, y las páginas no saben cómo se dibuja el menú.

Errores comunes

  • Usar una etiqueta sin agregar el componente a imports.
  • Cambiar el ancho del menú y olvidar el margen del contenido: el menú tapa el contenido.

Resumen

Partir una plantilla grande en componentes = HTML más corto, cada pieza con un solo trabajo y dependencias visibles en imports.

Preguntas de entrevista

1. ¿Por qué dividir una página en componentes más pequeños?

Respuesta:

Porque cada pieza queda pequeña y con una sola responsabilidad: es más fácil de leer, de probar y de reutilizar, y un cambio en el menú no obliga a tocar la página. Se paga con más archivos, pero cada uno es corto.

2. ¿Qué pasa si usas <gifs-side-menu /> y no importas el componente?

Respuesta:

Angular no reconoce la etiqueta y falla al compilar. En componentes standalone, cada plantilla sólo conoce lo que aparece en su propio imports: nada se hereda del componente padre.

3. ¿Para qué sirve el prefijo en el selector de un componente?

Respuesta:

Para evitar choques de nombre con etiquetas HTML nativas y con componentes de librerías, y para ubicar de dónde viene la etiqueta al leer una plantilla.


Módulo G4: Menú dinámico con @for y RouterLink

El problema que resuelve

El menú tenía un <a> escrito a mano por cada opción, todos casi iguales. Agregar una opción significaba copiar y pegar HTML. La idea es guardar las opciones en un arreglo y dejar que la plantilla dibuje un enlace por cada una.

Los datos

interface MenuOption {
  icon: string
  label: string
  route: string
  subLabel: string
}

menuOptions: MenuOption[] = [
  { icon: 'fa-solid fa-chart-line', label: 'Trending', route: 'trending', subLabel: 'Popular gifs' },
  { icon: 'fa-solid fa-magnifying-glass', label: 'Search', route: 'search', subLabel: 'Find gifs' },
]

La interface define la forma de cada opción: TypeScript avisa si falta un campo o si se escribe mal.

La plantilla

@for (option of menuOptions; track option.route) {
  <a [routerLink]="option.route"
     routerLinkActive="bg-blue-800"
     [routerLinkActiveOptions]="{ exact: true }">
    <i [class]="option.icon"></i>
    <span>{{ option.label }}</span>
    <span>{{ option.subLabel }}</span>
  </a>
}
  • @for repite el bloque por cada opción. track option.route le da a Angular una identificación única de cada elemento, para no redibujar todo cuando algo cambia.
  • [routerLink] convierte el <a> en un enlace de Angular: navega sin recargar la página.
  • routerLinkActive="bg-blue-800" agrega esa clase al enlace de la página actual, y así el menú muestra dónde estás.
  • exact: true pide que el enlace esté activo sólo si la URL coincide completa. Aquí no cambia el resultado, pero evita que un enlace "padre" quede marcado en todas sus páginas hijas.

Qué se importa

imports: [RouterLink, RouterLinkActive]

Se importan las dos directivas que se usan, no RouterModule completo.

Con y sin barra inicial

route: 'trending'    // relativa: queda en /dashboard/trending
route: '/trending'   // desde la raíz: apunta a /trending, que no existe

El menú vive dentro del dashboard, así que 'trending' se cuenta desde /dashboard. Con barra al inicio, la ruta se cuenta desde la raíz de la app.

Array simple o signal

El arreglo es fijo: no cambia mientras la app corre, así que una propiedad normal alcanza. Si las opciones cambiaran (por ejemplo, según el usuario), el arreglo iría dentro de un signal para que la plantilla se actualice sola.

Errores comunes

  • Poner la barra inicial en route.
  • Importar RouterModule en vez de RouterLink y RouterLinkActive.
  • Olvidar routerLinkActive: el menú no marca la página actual.

Resumen

Datos en un arreglo + @for + [routerLink] = un menú que crece agregando una línea, no copiando HTML. Más sobre @for y navegación en el Módulo A4.

Preguntas de entrevista

1. ¿Para qué sirve track en un @for?

Respuesta:

Le dice a Angular cómo identificar cada elemento de la lista. Así, cuando la lista cambia, reutiliza los elementos que siguen igual y sólo crea o quita los que cambiaron, en vez de dibujar todo de nuevo. Debe ser un valor único y estable, como un id.

2. ¿Qué hace routerLinkActive y para qué sirve exact: true?

Respuesta:

routerLinkActive agrega una clase CSS al enlace cuando su ruta es la actual, lo que permite resaltar la opción activa. Con exact: true, el enlace sólo se considera activo si la URL coincide por completo, no si apenas empieza igual.

3. ¿Qué diferencia hay entre routerLink="trending" y routerLink="/trending"?

Respuesta:

Sin barra, la ruta es relativa a donde está el enlace (aquí, dentro de /dashboard). Con barra, es absoluta y se cuenta desde la raíz. Es un error típico y la app deja de ir a la página que esperabas.

4. ¿Por qué routerLink y no un href normal?

Respuesta:

Un href pide la página nueva al servidor y recarga toda la app. routerLink deja que el enrutador de Angular cambie la vista sin recargar, conservando el estado y siendo mucho más rápido.


Módulo G5: Environments y path alias

El problema que resuelve

Hay valores que cambian según dónde corre la app: el nombre de la empresa, el eslogan, y más adelante las URLs de una API o sus claves. No conviene escribirlos sueltos por todo el código. Los environments son archivos que guardan esos valores en un solo lugar, uno por cada modo (desarrollo y producción).

Crear los archivos

El comando de Angular CLI los crea y deja la configuración lista:

ng generate environment

Obtienes:

  • src/environments/environment.ts: valores de producción.
  • src/environments/environment.development.ts: valores de desarrollo.
  • En angular.json, dentro de la configuración development, un reemplazo de archivos (fileReplacements).
"fileReplacements": [
  {
    "replace": "src/environments/environment.ts",
    "with": "src/environments/environment.development.ts"
  }
]

Cómo se usa

import { environment } from '@environments/environment'

export class SideMenuHeaderComponent {
  envs = environment
}
<h1>{{ envs.companyName }}<span>{{ envs.companyNameShort }}</span>.</h1>
<p>{{ envs.companySlogan }}</p>

Siempre se importa environment.ts. En un build de desarrollo, Angular cambia ese archivo por environment.development.ts por ti. Por eso nunca se importa el de desarrollo directamente: el código quedaría atado a ese modo.

Path alias: rutas cortas

Sin alias, el import sube carpetas a mano:

import { environment } from '../../../../environments/environment'

Con alias, la ruta sale desde un nombre fijo:

import { environment } from '@environments/environment'

Se configura en tsconfig.json:

"compilerOptions": {
  "paths": {
    "@environments/*": ["./src/environments/*"]
  }
}

Sin baseUrl

Antes, paths iba acompañado de "baseUrl": ".". TypeScript 6 marca baseUrl como obsoleto. Ya no hace falta: paths funciona solo, siempre que cada destino empiece con ./ (la ruta se cuenta desde el tsconfig.json). El precio es que ya no puedes importar "desde la raíz" con rutas tipo src/app/...; con alias no se nota.

Errores comunes

  • Dejar baseUrl en el tsconfig.json: TypeScript avisa que está obsoleto.
  • Escribir el destino del alias sin ./: sin baseUrl, el alias no resuelve.
  • Importar environment.development.ts directo: el modo producción quedaría usando valores de desarrollo.
  • Guardar claves secretas en estos archivos: terminan dentro del código que baja el navegador.

Resumen

Los environments agrupan los valores que cambian por modo, y Angular elige el archivo correcto al compilar. El alias @environments/* acorta el import y se configura sólo con paths.

Preguntas de entrevista

1. ¿Cómo sabe Angular qué archivo de environment usar?

Respuesta:

Con fileReplacements en angular.json. Tú siempre importas environment.ts; en el build de desarrollo, Angular reemplaza ese archivo por environment.development.ts antes de compilar. El código no cambia, sólo el archivo que entra al paquete final.

2. ¿Es seguro poner una clave secreta en un environment?

Respuesta:

No. Los archivos de environment se compilan dentro del código que se descarga en el navegador, y cualquiera puede leerlos. Sólo van valores públicos (URLs, nombres, banderas). Lo secreto debe vivir en un servidor.

3. ¿Qué es un path alias y qué ventaja tiene?

Respuesta:

Es un nombre corto para una carpeta, definido en paths de tsconfig.json (@environments/* apunta a ./src/environments/*). Evita cadenas de ../../.., que son difíciles de leer y se rompen si mueves el archivo de carpeta.

4. ¿Qué hago si TypeScript avisa que baseUrl está obsoleto?

Respuesta:

Quitarlo y dejar sólo paths, con cada destino relativo al tsconfig.json (empezando por ./). Los alias siguen funcionando igual.


Módulo G6: Lista de GIFs con input.required y signal

El problema que resuelve

La página Trending tiene que mostrar una cuadrícula de imágenes. Si el HTML de la cuadrícula y de cada imagen vive dentro de la página, no se puede reutilizar (la página Search va a necesitar la misma lista). La solución es separar quién tiene los datos de quién los dibuja.

Las tres piezas

trending-page          tiene los datos: gifs = signal([...])
 └── gif-list          recibe la lista y repite un elemento por imagen
      └── gif-list-item     recibe una URL y dibuja una imagen

Los datos bajan, de padre a hijo, por entradas (input):

// trending-page.component.ts
gifs = signal(imageUrls)
<!-- trending-page.component.html -->
<gif-list [gifs]="gifs()" />
// gif-list.component.ts
public readonly gifs = input.required<string[]>()
<!-- gif-list.component.html -->
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
  @for (gif of gifs(); let idx = $index; track idx) {
    <gif-list-item [imageUrl]="gif" />
  }
</div>
// gif-list-item.component.ts
public readonly imageUrl = input.required<string>()
<!-- gif-list-item.component.html -->
<div>
  <img class="h-auto max-w-full rounded-base" [src]="imageUrl()" alt="">
</div>

Por ahora la lista usa doce imágenes de prueba escritas en el propio archivo; la idea es sustituirlas después por los GIFs de una API.

input.required

input<T>() crea una entrada opcional. input.required<T>() crea una que el padre tiene que entregar: si usas <gif-list /> sin [gifs], el proyecto no compila. Como imageUrl y gifs no tienen sentido vacíos, required es lo correcto.

Una entrada es un signal de sólo lectura: se lee con paréntesis (imageUrl(), gifs()) y no se asigna desde el hijo.

Cuadrícula responsive con Tailwind

grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4

Tailwind parte del móvil: grid-cols-1 vale para todas las pantallas, y cada prefijo (sm:, md:) lo cambia desde ese ancho hacia arriba. Resultado: una columna en pantallas pequeñas, dos desde sm y tres desde md.

track por posición

track idx identifica cada imagen por su posición en la lista. Con una lista fija es suficiente. Cuando los datos vengan de una API, es mejor usar un valor propio de cada elemento (por ejemplo, el id del GIF): si la lista se reordena, Angular puede mover los elementos en vez de reconstruirlos.

Errores comunes

  • Leer una entrada sin paréntesis (imageUrl en vez de imageUrl()): se obtiene la función, no el valor.
  • Olvidar la entrada obligatoria al usar el componente.
  • Poner la cuadrícula dentro de la página: no se puede reutilizar.

Resumen

La página guarda los datos en un signal; gif-list y gif-list-item sólo reciben y dibujan, cada uno con una entrada obligatoria. Más sobre input() y señales en el Módulo A4.

Preguntas de entrevista

1. ¿Qué diferencia hay entre input() e input.required()?

Respuesta:

input() es una entrada opcional (puede quedar sin valor o con uno por defecto). input.required() obliga al padre a pasarla: si falta, el compilador lo avisa. Sirve para datos sin los que el componente no tiene sentido.

2. ¿Por qué la página guarda los datos y no gif-list?

Respuesta:

Porque así gif-list es un componente de presentación: recibe una lista y la dibuja, sin saber de dónde viene. Se puede reutilizar con otra fuente de datos (la página de búsqueda) sin cambiarlo. Quien conoce el origen de los datos es la página.

3. ¿Qué riesgo tiene track $index con datos reales?

Respuesta:

Que Angular asocia cada elemento con su posición. Si la lista se reordena o se quita un elemento del medio, los elementos que siguen cambian de posición y Angular los trata como "otros" y los vuelve a dibujar, o mezcla estado (por ejemplo, un campo de texto). Con un id propio, Angular sigue a cada elemento aunque cambie de lugar.

4. ¿Por qué Tailwind se escribe "desde móvil hacia arriba"?

Respuesta:

Porque las clases sin prefijo valen para todos los anchos, y los prefijos (sm:, md:) sólo aplican a partir de cierto ancho. Así defines primero el caso más simple (móvil) y vas agregando cambios para pantallas más grandes, sin escribir reglas para cada tamaño.

Clone this wiki locally