Skip to content

04 country app

Janio Isacura edited this page Oct 16, 2026 · 6 revisions

Parte 4: Country 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 3, con la app de GIFs, está en 03-gifs-app. Proyecto: 04-country-app/.

Es una aplicación Angular 22 creada con ng new, con Tailwind CSS para las clases de utilidad y daisyUI para los componentes ya diseñados (botones, tarjetas, barras de navegación). Esta página va creciendo: cada módulo nuevo se agrega al final.


Módulo C1: Instalar Tailwind CSS y daisyUI

El problema que resuelve

Con Tailwind CSS se estiliza escribiendo clases pequeñas en el HTML (text-3xl font-bold). Es muy flexible, pero un botón bonito termina con diez clases repetidas en cada sitio donde aparece.

daisyUI resuelve eso: es un complemento (plugin) de Tailwind que agrega nombres de clase para componentes completos. En vez de diez clases, escribes una o dos:

<!-- Sólo Tailwind -->
<button class="px-4 py-2 rounded-lg bg-blue-600 text-white font-semibold hover:bg-blue-700">
  Buscar
</button>

<!-- Tailwind con daisyUI -->
<button class="btn btn-primary">Buscar</button>

Las dos cosas conviven: puedes usar btn de daisyUI y ajustar con clases de Tailwind encima (btn btn-primary mt-4).

Qué se instala

Paquete Para qué sirve
tailwindcss El motor de clases de utilidad
@tailwindcss/postcss La pieza que conecta Tailwind con el compilador de Angular
postcss La herramienta que procesa el CSS y que usa la anterior
daisyui Las clases de componentes (btn, card, navbar...)
npm install tailwindcss @tailwindcss/postcss postcss
npm install -D daisyui@latest

daisyUI 5 necesita Tailwind 4: las versiones de este proyecto son compatibles. La guía oficial paso a paso está en daisyUI: install for Angular.

La configuración, en tres archivos

1. .postcssrc.json le dice a Angular que procese el CSS con Tailwind:

{
  "plugins": {
    "@tailwindcss/postcss": {}
  }
}

2. src/tailwind.css importa Tailwind y activa daisyUI:

@import "tailwindcss";
@plugin "daisyui";

3. angular.json carga ese archivo antes de los estilos globales:

"styles": [
  "src/tailwind.css",
  "src/styles.scss"
]

Tailwind va en su propio archivo .css y no dentro de styles.scss, por la misma razón que se explica en el Módulo G1: Sass interpreta el @import por su cuenta y lo avisa como obsoleto.

Comprobar que funciona

  1. Arranca el servidor con npm start (o compila con npm run build). En la salida de la compilación aparece una línea con el nombre y la versión de daisyUI, señal de que el plugin se cargó.

  2. Escribe un componente de daisyUI en una plantilla:

    <button class="btn btn-primary">Hola</button>
  3. Mira la página: debe verse un botón con estilo. Si sale un botón sin estilo, la clase no llegó al CSS (ver Errores comunes).

Tailwind y daisyUI sólo incluyen en el CSS final las clases que realmente aparecen en tus archivos. Por eso un proyecto recién creado, sin ningún btn en las plantillas, genera un CSS muy pequeño.

Temas

daisyUI trae temas de color listos, y se cambian con el atributo data-theme de un elemento (normalmente <html>). Cómo elegirlos, cuáles incluir y cómo cambiarlos mientras la app corre se explica en el Módulo C2.

El aviso del editor en @plugin

VS Code puede subrayar @plugin "daisyui" con un aviso de que es una regla CSS desconocida. Viene del validador de CSS del editor, que no conoce las directivas propias de Tailwind 4 (@plugin, @theme, @source). No afecta al build. La extensión Tailwind CSS IntelliSense lo quita y además autocompleta las clases de daisyUI.

Errores comunes

  • Meter @import "tailwindcss" dentro de styles.scss: Sass lo interpreta y avisa. Va en un .css aparte.
  • Poner tailwind.css después de styles.scss en angular.json: lo que se carga al final gana en un empate, y tus estilos globales propios quedarían por debajo de los de Tailwind.
  • Olvidar el .postcssrc.json: Angular no sabe que debe procesar el CSS con Tailwind y las clases no tienen efecto.
  • Usar una versión de daisyUI que no corresponde a la de Tailwind: daisyUI 5 funciona con Tailwind 4; con Tailwind 3 se configura de otra forma.
  • Cambiar .postcssrc.json o angular.json con el servidor encendido: hay que reiniciar npm start para que los lea.
  • Construir nombres de clase a trozos en el código ('btn-' + color): Tailwind busca el texto completo y no encuentra la clase. Se escriben enteras.

Resumen

Tailwind CSS da las clases pequeñas y daisyUI agrega, encima, las clases de componentes. Se instalan cuatro paquetes y se configuran tres archivos: .postcssrc.json (procesar con Tailwind), tailwind.css (@import y @plugin) y angular.json (cargarlo antes que los estilos propios). Sólo se incluyen en el CSS las clases que de verdad se usan.

Preguntas de entrevista

1. ¿Qué agrega daisyUI sobre Tailwind CSS?

Respuesta:

Tailwind ofrece clases de utilidad pequeñas (px-4, bg-blue-600). daisyUI es un plugin que agrega, encima, clases de componentes completos (btn, card, navbar) y temas de color. No reemplaza a Tailwind: se pueden combinar, y las clases de utilidad sirven para ajustar los componentes.

2. ¿Por qué @plugin "daisyui" sale subrayado en VS Code si el proyecto compila?

Respuesta:

Porque el validador de CSS del editor no conoce las directivas de Tailwind 4, y las marca como reglas desconocidas. Es un aviso del editor, no del compilador: el build funciona y el plugin se carga. La extensión Tailwind CSS IntelliSense lo resuelve.

3. ¿Por qué un proyecto sin ningún btn en las plantillas casi no genera CSS de daisyUI?

Respuesta:

Porque Tailwind busca en los archivos del proyecto los nombres de clase que se usan y sólo genera el CSS de esas. Si ninguna plantilla tiene btn, esas reglas no se incluyen. Por eso tampoco sirve armar nombres de clase por partes en el código ('btn-' + color): el texto completo debe aparecer escrito.

4. ¿Qué hace .postcssrc.json en un proyecto Angular con Tailwind?

Respuesta:

Registra el plugin @tailwindcss/postcss en el proceso de CSS de Angular. Sin ese archivo, Angular compila el CSS tal cual y las directivas de Tailwind (@import "tailwindcss", @plugin) no se convierten en estilos reales.


Módulo C2: Elegir y cambiar el tema de daisyUI

El problema que resuelve

Un tema es un conjunto de colores (fondo, texto, botones, bordes) con nombre. Con daisyUI cambiar de light a dark, a sunset o a nord no obliga a tocar ninguna clase: los componentes (btn, card...) usan los colores del tema activo, y al cambiar el tema cambian todos a la vez.

Hay tres decisiones separadas, y conviene no mezclarlas:

Pregunta Dónde se decide
¿Qué temas existen en mi CSS? tailwind.css, en las opciones del plugin
¿Cuál se usa ahora? El atributo data-theme
¿Cómo lo cambia el usuario? Un poco de código (o un input especial)

Paso 1: elegir qué temas se generan

Por defecto el plugin incluye sólo dos: light (el predeterminado) y dark (el que se usa cuando el sistema del usuario está en modo oscuro). Para cambiarlo, se pasan opciones al plugin:

@import "tailwindcss";
@plugin "daisyui" {
  themes: light --default, dark --prefersdark, sunset;
}

Cada elemento de la lista es un nombre de tema, con banderas opcionales:

Bandera Qué significa
--default Es el tema que se usa cuando nada indica otro
--prefersdark Es el que se usa si el sistema está en modo oscuro y la página no fija un tema
(ninguna) El tema está disponible, pero sólo se activa si se pide con data-theme

Otras formas válidas:

/* un tema como predeterminado y otro para el modo oscuro */
@plugin "daisyui" {
  themes: nord --default, abyss --prefersdark;
}

/* todos los temas */
@plugin "daisyui" {
  themes: all;
}

Sólo se generan los temas de la lista. Si el CSS no lleva un tema, pedirlo con data-theme no hace nada. Esto se comprobó compilando el proyecto con themes: nord --default, abyss --prefersdark: en el CSS resultante aparecían nord y abyss y no aparecían light ni dark.

Paso 2: elegir el tema activo con data-theme

<!-- src/index.html -->
<html lang="en" data-theme="sunset">

Con ese atributo, toda la página usa sunset. Reglas útiles:

  • El atributo gana sobre --default y --prefersdark: si está, el modo oscuro del sistema deja de importar.
  • Funciona en cualquier elemento, no sólo en <html>. Una sección con data-theme="dark" se ve oscura dentro de una página clara.
  • Sin atributo, se aplican las banderas: --default, o --prefersdark si el sistema está en modo oscuro.

Los temas que trae daisyUI

light, dark, cupcake, bumblebee, emerald, corporate, synthwave, retro, cyberpunk, valentine, halloween, garden, forest, aqua, lofi, pastel, fantasy, wireframe, black, luxury, dracula, cmyk, autumn, business, acid, lemonade, night, coffee, winter, dim, nord, sunset, caramellatte, abyss y silk. Se ven todos juntos en la página de temas de daisyUI.

Paso 3: cambiar el tema mientras la app corre

Cambiar de tema es cambiar el valor de data-theme. En Angular conviene un servicio pequeño que guarde la elección en una señal y la escriba en <html>:

import { DOCUMENT } from '@angular/common'
import { effect, inject, Injectable, signal } from '@angular/core'

const THEME_KEY = 'theme'

@Injectable({ providedIn: 'root' })
export class ThemeService {
  private document = inject(DOCUMENT)

  theme = signal<string>(localStorage.getItem(THEME_KEY) ?? 'sunset')

  private applyTheme = effect(() => {
    const theme = this.theme()
    this.document.documentElement.setAttribute('data-theme', theme)
    localStorage.setItem(THEME_KEY, theme)
  })
}

Cómo funciona:

  1. theme es una señal con el tema actual. Arranca con el que haya guardado en localStorage, o con sunset si no hay ninguno.
  2. El effect se ejecuta cada vez que theme cambia: escribe el atributo en <html> y guarda la elección para la próxima visita.
  3. inject(DOCUMENT) es la forma de Angular de llegar al documento, en lugar de usar document directamente.

Y en cualquier componente:

themeService = inject(ThemeService)
<button class="btn" (click)="themeService.theme.set('nord')">Nord</button>
<button class="btn" (click)="themeService.theme.set('sunset')">Sunset</button>

Alternativa sin código: theme-controller

daisyUI incluye una clase, theme-controller, que hace el cambio solo con HTML. Un input con esa clase y el nombre del tema en value activa ese tema cuando está marcado:

<input type="radio" name="theme" class="btn theme-controller" aria-label="Nord" value="nord">
<input type="radio" name="theme" class="btn theme-controller" aria-label="Sunset" value="sunset">

Sirve para un prototipo, pero no recuerda la elección al recargar la página. Con el servicio sí se puede guardar.

Errores comunes

  • Pedir un tema que no está en la lista themes: el atributo no hace nada, porque ese CSS no se generó.
  • Olvidar --default: el plugin no sabe cuál usar cuando no hay data-theme y la página puede quedar con colores inesperados.
  • Un nombre mal escrito en data-theme o en la lista (synthwave, no synth-wave): no falla, simplemente no aplica.
  • Fijar data-theme y esperar que el modo oscuro del sistema lo cambie: el atributo tiene prioridad, así que el sistema deja de influir.
  • Reiniciar la app y perder la elección: si sólo se guarda en memoria, vuelve al predeterminado. Hay que persistirla (localStorage).
  • Cambiar tailwind.css con el servidor encendido: hay que reiniciar npm start para que lo lea.

Resumen

Elegir un tema son tres cosas: decidir cuáles se generan (opciones del plugin en tailwind.css, con --default y --prefersdark), cuál está activo (atributo data-theme, en <html> o en cualquier elemento) y cómo se cambia (un servicio con una señal y un effect que escribe el atributo, o theme-controller sin código). Un tema que no está en la lista no se puede activar.

Preguntas de entrevista

1. ¿Para qué sirven --default y --prefersdark en las opciones de daisyUI?

Respuesta:

--default marca el tema que se usa cuando la página no indica otro. --prefersdark marca el que se usa cuando el sistema operativo del usuario está en modo oscuro y la página no fija un tema con data-theme. Juntos permiten que la app respete la preferencia del sistema sin código.

2. Puse data-theme="nord" y no cambia nada. ¿Qué miro?

Respuesta:

Primero, que nord esté en la lista themes del plugin en tailwind.css: daisyUI sólo genera el CSS de los temas listados, y sin él el atributo no tiene efecto. Después, que el nombre esté bien escrito y que el servidor se haya reiniciado tras tocar tailwind.css.

3. ¿Por qué el servicio usa un effect y no escribe el atributo en el método que cambia el tema?

Respuesta:

Porque el effect reacciona al valor de la señal, sin importar quién lo cambió: un botón, el valor recuperado de localStorage al arrancar o cualquier otro componente. Así hay un solo punto donde se toca el DOM y guardar la elección no se puede olvidar. Es un efecto secundario, que es justo para lo que sirve effect (a diferencia de computed, que sólo calcula valores).

4. ¿Qué diferencia hay entre un tema elegido con data-theme y uno elegido con --prefersdark?

Respuesta:

--prefersdark depende del sistema del usuario y sólo se aplica si la página no fija un tema. data-theme es una orden explícita de la página y tiene prioridad: si está presente, el modo oscuro del sistema se ignora. Por eso una app con selector de tema suele fijar el atributo, y una que sólo quiere respetar al sistema deja que actúen las banderas.


Módulo C3: Rutas por funcionalidad con loadChildren

Este módulo está escrito como instructivo: al final hay una lista de pasos para organizar las rutas de otro proyecto de la misma forma.

El problema que resuelve

Al principio, todas las rutas viven en un solo archivo, app.routes.ts. Con dos o tres pantallas no pasa nada, pero a medida que la app crece ese archivo se llena de rutas de países, de usuarios, de pedidos... y cualquiera que lo abra tiene que leer todo para encontrar lo suyo.

La solución es que cada funcionalidad (feature) tenga su propio archivo de rutas, y que el archivo principal sólo diga "todo lo que empiece por country se resuelve allá".

Antes (ejemplo)                Después
app.routes.ts                  app.routes.ts          ← sólo lo general
 ├─ ''                          ├─ ''                  (inicio)
 ├─ country                     ├─ country  ──────┐    "esto lo resuelve
 ├─ country/by-capital          └─ **             │     otro archivo"
 ├─ country/by-region                              ▼
 ├─ country/by-name            country/country.routes.ts
 └─ **                          ├─ ''
                                ├─ by-capital
                                └─ by-region

Las piezas del proyecto

src/app/
├── app.routes.ts                       rutas generales (inicio, comodín)
├── app.html                            <router-outlet />
├── shared/pages/home-page/             pantalla de inicio
└── country/
    ├── country.routes.ts               rutas de la funcionalidad "country"
    └── pages/by-capital/               pantalla "por capital"

Paso 1: las rutas generales

// app.routes.ts
export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./shared/pages/home-page/home-page.component'),
  },
  {
    path: 'country',
    loadChildren: () => import('./country/country.routes'),
  },
  {
    path: '**',
    redirectTo: '',
  },
]
Ruta Qué hace
path: '' + loadComponent La pantalla de inicio. Se carga sólo cuando se visita
path: 'country' + loadChildren Delega: todo lo que empiece por /country lo resuelve otro archivo de rutas, que también se carga cuando hace falta
path: '**' + redirectTo: '' Comodín: cualquier dirección que no coincida con nada manda al inicio

El orden importa: Angular prueba las rutas de arriba hacia abajo y se queda con la primera que coincide. Por eso el comodín ** va siempre al final.

Paso 2: las rutas de la funcionalidad

// country/country.routes.ts
import { Routes } from '@angular/router'
import { ByCapitalComponent } from './pages/by-capital/by-capital.component'

export const countryRoutes: Routes = [
  {
    path: '',
    component: ByCapitalComponent,
  },
]

export default countryRoutes
  • Aquí path: '' significa "justo /country", porque este archivo ya está colgado de country: las rutas de un archivo hijo se suman a la ruta del padre.
  • En el proyecto, el arreglo todavía no lleva el tipo (countryRoutes = [...]); con : Routes el editor autocompleta y avisa de errores, por eso se muestra tipado.
  • El archivo termina con export default: es lo que permite escribir import('./country/country.routes') en el padre sin indicar el nombre de nada.

En el Módulo C4 esta ruta '' deja de mostrar una sola pantalla: pasa a ser un layout con rutas hijas y su propio comodín. Desde entonces, /country/otra ya no cae en el inicio sino en by-capital.

Cómo se arman las direcciones

/               →  app.routes: path ''                    → HomePage
/country        →  app.routes: 'country'
                      └─ country.routes: ''               → ByCapital
/country/otra   →  'country' coincide, pero no hay hija 'otra'
                      └─ cae en '**' de app.routes        → redirige a '/'
/lo-que-sea     →  '**'                                   → redirige a '/'

Esto se comprobó abriendo la app en el navegador: / muestra el inicio, /country muestra la pantalla de capital, y tanto /lo-que-sea como /country/otra terminan en el inicio.

Paso 3: el lugar donde se dibuja

<!-- app.html -->
<router-outlet />

<router-outlet /> es el hueco donde Angular pone el componente de la ruta activa. Al cambiar de dirección, cambia lo que hay en ese hueco; el resto de la plantilla, si lo hubiera, no se toca.

export default y export class: cuándo cada uno

Archivo Cómo se carga Exportación
HomePageComponent loadComponent: () => import(...) (perezosa) export default class
country.routes.ts loadChildren: () => import(...) (perezosa) export default del arreglo
ByCapitalComponent import { ByCapitalComponent } from ... (normal) export class con nombre

La regla del proyecto: export default sólo para lo que se carga con import() desde una ruta. Lo que se usa con un import normal (como ByCapitalComponent, que ya viaja dentro del archivo de rutas) lleva export class con nombre.

Lo que se gana: carga perezosa

Al compilar, Angular genera un archivo separado por cada import() de una ruta:

Lazy chunk files    | Names
chunk-….js          | country-routes
chunk-….js          | home-page-component

El código de country no se descarga hasta que el usuario entra en /country. En una app grande, eso hace que la primera carga sea mucho más liviana.

Instructivo: organizar otra app así

  1. Crea una carpeta por funcionalidad (country/, users/...), con sus páginas dentro (pages/).
  2. En cada una, un archivo <feature>.routes.ts con un arreglo tipado como Routes, y export default de ese arreglo.
  3. En app.routes.ts, una entrada por funcionalidad: { path: 'country', loadChildren: () => import('./country/country.routes') }.
  4. Las pantallas sueltas que no pertenecen a una funcionalidad (inicio) van en shared/pages/ con loadComponent.
  5. El comodín ** con redirectTo va al final de app.routes.ts.
  6. En la plantilla raíz, un <router-outlet />.
  7. Para añadir una pantalla nueva a la funcionalidad, se agrega una entrada en su *.routes.ts (por ejemplo { path: 'by-region', component: ... }) y no se toca app.routes.ts.

Errores comunes

  • Poner el comodín ** antes que otras rutas: como Angular se queda con la primera que coincide, todo lo que esté debajo deja de alcanzarse.
  • Escribir path: '/country' con barra inicial: Angular rechaza la configuración al arrancar. Los path van sin barra al principio.
  • Repetir el prefijo en la ruta hija: si el padre ya es country, la hija es path: 'by-capital', no path: 'country/by-capital'.
  • Olvidar el export default en el archivo de rutas: import('./...') no encuentra qué usar y la ruta falla al cargarse.
  • No tipar el arreglo como Routes: funciona, pero se pierde el autocompletado y el aviso de errores en las propiedades (pth en vez de path pasaría desapercibido).
  • Olvidar <router-outlet />: la ruta cambia en la barra de direcciones, pero no se dibuja nada.
  • Usar export default en componentes que no se cargan con import(): al importarlos con llaves en otra plantilla, Angular dice que el elemento no se conoce.

Resumen

Cada funcionalidad tiene su propio archivo de rutas, y app.routes.ts sólo la enlaza con loadChildren, de modo que el archivo principal se mantiene corto y el código de cada funcionalidad se descarga cuando se usa. Las rutas hijas se suman a la del padre ('' dentro de country es /country), el comodín va al final, y export default se usa sólo para lo que se carga con import() desde una ruta.

Preguntas de entrevista

1. ¿Qué diferencia hay entre loadComponent y loadChildren?

Respuesta:

loadComponent carga un componente de forma perezosa, para una sola pantalla. loadChildren carga un conjunto de rutas (con sus propias pantallas y subrutas) cuando se visita el prefijo indicado. En ambos casos el código no se descarga hasta que hace falta; la diferencia es el tamaño de lo que se delega.

2. ¿Qué significa path: '' dentro de country.routes.ts?

Respuesta:

Que la ruta coincide justo con el prefijo del padre: como el archivo está colgado de country, path: '' equivale a /country. Las rutas de un archivo hijo se suman a la del padre, así que no hay que repetir country en ellas.

3. ¿Por qué el comodín ** va al final?

Respuesta:

Porque el enrutador recorre las rutas en orden y se queda con la primera que coincide. ** coincide con cualquier dirección: si estuviera antes, taparía a todas las que están debajo y nunca se alcanzarían.

4. ¿Qué pasa con /country/otra si country.routes.ts sólo define ''?

Respuesta:

El prefijo country coincide, pero no hay ninguna hija para otra. El enrutador sigue buscando en el resto de las rutas de app.routes.ts y termina en el comodín **, que redirige al inicio. Se comprobó en el navegador: la dirección acaba mostrando la pantalla de inicio.

5. ¿Para qué sirve el export default en un archivo de rutas?

Respuesta:

Para poder escribir loadChildren: () => import('./country/country.routes') sin nombrar la exportación: Angular toma la exportación por defecto del módulo cargado. Sin ella habría que indicar el nombre con un .then(m => m.countryRoutes).

6. ¿Qué se gana al dividir las rutas por funcionalidad?

Respuesta:

Dos cosas: orden (cada funcionalidad tiene sus rutas en un solo sitio y app.routes.ts no crece) y rendimiento (con loadChildren, Angular genera un archivo aparte por funcionalidad y sólo lo descarga cuando el usuario entra, por lo que la carga inicial es menor).


Módulo C4: Un layout con rutas hijas

Este módulo está escrito como instructivo: al final hay una lista de pasos para repetirlo en otra funcionalidad.

El problema que resuelve

En el Módulo C3 cada ruta de country mostraba una sola pantalla. Pero todas las pantallas de esa funcionalidad (por capital, por país, por región) van a compartir lo mismo: un menú superior. Si cada página lo incluyera por su cuenta, habría que repetirlo en todas y cambiarlo en todas.

La solución es un layout: un componente que dibuja lo común y deja un hueco para la pantalla que cambia.

┌──────────────────────────────────────────┐
│  CountryLayoutComponent                  │
│  ┌────────────────────────────────────┐  │
│  │ <country-top-menu />   ← siempre   │  │
│  └────────────────────────────────────┘  │
│  ┌────────────────────────────────────┐  │
│  │ <router-outlet />                  │  │
│  │   by-capital  |  by-country | ...  │  │ ← cambia según la ruta
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘

Paso 1: el layout

// country-layout.component.ts
@Component({
  selector: 'app-country-layout',
  imports: [RouterOutlet, TopMenuComponent],
  templateUrl: './country-layout.component.html',
})
export class CountryLayoutComponent {}
<!-- country-layout.component.html -->
<country-top-menu />
<section class="px-4">
  <router-outlet />
</section>

Dos detalles importantes:

  • El layout tiene su propio <router-outlet />: es donde se dibujarán las pantallas hijas. El que está en app.html dibuja el layout; el del layout dibuja la pantalla.
  • Hay que importar RouterOutlet en el layout, y también el componente del menú, porque las plantillas sólo conocen lo que está en imports.

Paso 2: las rutas con children

// country.routes.ts
export const countryRoutes: Routes = [
  {
    path: '',
    component: CountryLayoutComponent,
    children: [
      {
        path: 'by-capital',
        component: ByCapitalComponent,
      },
      {
        path: '**',
        redirectTo: 'by-capital',
      },
    ],
  },
]

export default countryRoutes

La ruta del layout tiene component y children a la vez: es lo que la convierte en un layout. Su path: '' no añade nada a la dirección, de modo que las hijas cuelgan directamente de /country.

Dirección Qué dibuja
/country/by-capital El layout con ByCapitalComponent en su hueco
/country Cae en el comodín hijo y redirige a /country/by-capital
/country/lo-que-sea También redirige a /country/by-capital
/lo-que-sea Está fuera de country: cae en el comodín general y vuelve al inicio

Esto se comprobó en el navegador: en /country la barra de direcciones termina en /country/by-capital, y en la página aparecen el layout, el menú y la pantalla de capital.

Dos comodines, dos alcances

Ahora hay dos **, y cada uno actúa en su nivel:

app.routes.ts      **  →  redirige al inicio ('')
country.routes.ts  **  →  redirige a 'by-capital'   (sólo dentro de /country)

El de la funcionalidad tiene prioridad dentro de /country, porque Angular entra primero en esa rama. Por eso, desde que existe el layout, /country/otra ya no cae en el inicio, como pasaba en el Módulo C3: cae en by-capital.

¿Por qué las hijas no usan loadComponent?

Las pantallas hijas se importan con un import normal y no de forma perezosa. La razón: el layout sólo se carga cuando se entra en country, y las pantallas de esa funcionalidad se necesitan justo en ese momento. Separarlas en archivos distintos no ahorra nada. La carga perezosa ya ocurre un nivel arriba, con loadChildren.

Por la misma regla del proyecto, CountryLayoutComponent y ByCapitalComponent llevan export class con nombre: se importan de forma normal en el archivo de rutas.

Instructivo: un layout para otra funcionalidad

  1. Crea el componente layout en <feature>/layouts/<nombre>-layout/.
  2. En su plantilla, pon lo común (menú, cabecera, barra lateral) y un <router-outlet /> para el contenido.
  3. Importa RouterOutlet y los componentes que use en el imports del layout.
  4. En <feature>.routes.ts, una ruta con path: '', component: <Layout> y children: [...] con las pantallas.
  5. Agrega un comodín ** como última hija, con redirectTo hacia la pantalla por defecto.
  6. Para una pantalla nueva, se añade una entrada en children; el layout no se toca.

Errores comunes

  • Olvidar el <router-outlet /> dentro del layout: el menú se ve, pero las pantallas nunca aparecen.
  • Poner el comodín antes que las demás hijas: captura todo y las hijas siguientes no se alcanzan.
  • Escribir redirectTo: '/by-capital' con barra: es una ruta absoluta desde la raíz de la aplicación, no desde country. Sin barra es relativa a donde está la regla.
  • Repetir el menú en cada página: es justo lo que el layout evita.
  • Poner el layout como ruta sin children: funciona como una pantalla más, pero no tiene dónde dibujar las demás.

Resumen

Un layout es una ruta con component y children: el componente dibuja lo común y su <router-outlet /> dibuja la pantalla hija que corresponda. Así el menú se escribe una sola vez. El comodín de la funcionalidad actúa dentro de /country y el general queda para el resto.

Preguntas de entrevista

1. ¿Cuándo usarías un layout con rutas hijas?

Respuesta:

Cuando varias pantallas comparten una estructura (menú, cabecera, barra lateral) y sólo cambia el contenido. El layout se escribe una vez y las rutas hijas se dibujan en su <router-outlet />. Evita repetir esa estructura en cada página y cambiarla en varios sitios.

2. ¿Cuántos <router-outlet /> hay en esta aplicación y para qué sirve cada uno?

Respuesta:

Dos. El de app.html dibuja la ruta de primer nivel (inicio o el layout de country). El del layout dibuja la pantalla hija (by-capital...). Cada <router-outlet /> dibuja las rutas del nivel en el que está.

3. ¿Qué pasa al entrar en /country, si sólo hay una hija llamada by-capital?

Respuesta:

La ruta del layout (path: '') coincide, pero ninguna hija coincide con una dirección vacía; entonces actúa el comodín hijo, que redirige a by-capital. La dirección termina siendo /country/by-capital y se ve el layout con esa pantalla.

4. ¿Qué diferencia hay entre el comodín de app.routes.ts y el de country.routes.ts?

Respuesta:

El alcance. El de country.routes.ts sólo se evalúa dentro de /country y manda a la pantalla por defecto de esa funcionalidad; el de app.routes.ts atiende todo lo demás y manda al inicio. Angular entra primero en la rama que coincide con el prefijo, así que dentro de /country manda el de la funcionalidad.


Módulo C5: Componentes compartidos y de funcionalidad

El problema que resuelve

A medida que crecen las pantallas, aparecen piezas que se repiten: un pie de página, un menú. La pregunta es dónde viven y quién las puede usar. Si todo está en una carpeta común, no se sabe qué depende de qué.

La regla: compartido o de la funcionalidad

Carpeta Para qué Ejemplo Selector
shared/components/ Piezas que usa toda la aplicación footer app-footer
<feature>/components/ Piezas que sólo usa esa funcionalidad country/components/top-menu country-top-menu
src/app/
├── app.html                      usa  <app-footer />
├── shared/
│   ├── components/footer/        FooterComponent        (toda la app)
│   └── pages/home-page/
└── country/
    ├── components/top-menu/      TopMenuComponent       (sólo country)
    ├── layouts/country-layout/   usa  <country-top-menu />
    └── pages/by-capital/

La razón de separar: si mañana se borra o se mueve la funcionalidad country, todo lo que es suyo se va con ella y no deja piezas sueltas. Lo que está en shared se supone usado por varias partes.

El prefijo del selector

El prefijo del selector (app- o country-) indica de dónde es el componente cuando se lo ve en una plantilla. <country-top-menu /> dice que es del menú de country; <app-footer /> es de toda la aplicación. Es una convención del proyecto, no una obligación del framework.

Cómo se usa un componente

Un componente standalone no se declara en ningún módulo: se importa donde se va a usar.

// app.ts
@Component({
  selector: 'app-root',
  imports: [RouterOutlet, FooterComponent],
  templateUrl: './app.html',
})
export class App {}
<!-- app.html -->
<main class="m-auto flex flex-grow flex-col min-h-screen">
  <router-outlet />
  <div class="flex-1 flex-grow">
    <app-footer />
  </div>
</main>

Dos pasos y los dos hacen falta: importarlo en el imports del componente que lo usa y escribir su etiqueta en la plantilla. Si falta el primero, Angular avisa de que el elemento no se conoce.

Aquí el pie de página queda en app.html, fuera de las rutas: aparece en todas las pantallas (inicio y country) sin que cada una lo incluya.

Un detalle del pie de página: el año

// footer.component.ts
export class FooterComponent {
  currentYear = new Date().getFullYear()
}
<p>Copyright © {{ currentYear }} Reskyon - All rights reserved</p>

El año sale de una propiedad de la clase y se muestra con doble llave. Es un error típico al copiar ejemplos pensados para React escribir {new Date().getFullYear()} con llave simple: en Angular, la llave simple se interpreta como el inicio de una expresión especial y la plantilla no compila.

Un componente por responsabilidad

Componente Responsabilidad
App La raíz: outlet principal y pie de página
CountryLayoutComponent La estructura de la funcionalidad (menú + hueco)
TopMenuComponent El menú de navegación
ByCapitalComponent La pantalla de búsqueda por capital

Cada uno hace una cosa, y por eso se puede cambiar o reemplazar sin tocar los demás.

Instructivo: añadir un componente compartido

  1. Decide el alcance: si lo usará toda la app va en shared/components/<nombre>/; si es de una funcionalidad, en <feature>/components/<nombre>/.
  2. Crea el componente con un selector con el prefijo que corresponda (app-…, <feature>-…).
  3. Exporta la clase con nombre (export class): se importará en otro componente, no por una ruta.
  4. Impórtalo en el imports del componente que lo va a usar.
  5. Escribe la etiqueta en su plantilla.

Errores comunes

  • Escribir la etiqueta sin importar el componente: Angular dice que el elemento no se conoce.
  • Importar el componente y no usarlo: el compilador avisa de que el import sobra.
  • Meter en shared algo que sólo usa una funcionalidad: shared se vuelve un cajón de todo y se pierde la ventaja de separar.
  • Usar export default en estos componentes: el default es sólo para lo que se carga por una ruta con import().
  • Llave simple en la plantilla ({ expresión }): hay que usar doble llave ({{ expresión }}).

Resumen

Lo que usa toda la aplicación va en shared/; lo que usa una funcionalidad, en su propia carpeta components/. El prefijo del selector deja ver el origen. Un componente standalone se importa en el imports de quien lo usa y se escribe su etiqueta en la plantilla; el pie de página, al estar en app.html, aparece en todas las pantallas.

Preguntas de entrevista

1. ¿Cómo decides si un componente va en shared o en una funcionalidad?

Respuesta:

Por quién lo usa. Si lo necesitan varias partes de la aplicación (un pie de página, un botón), va en shared. Si sólo tiene sentido dentro de una funcionalidad (el menú de países), va con ella. Así, borrar la funcionalidad no deja componentes huérfanos y shared no se convierte en un cajón de todo.

2. ¿Qué hace falta para usar un componente standalone en una plantilla?

Respuesta:

Dos cosas: importar su clase en el imports del componente que lo usa, y escribir su selector como etiqueta en la plantilla. Con sólo la etiqueta, Angular no sabe qué componente es; con sólo el import, no se dibuja.

3. ¿Por qué el pie de página está en app.html y no dentro de cada pantalla?

Respuesta:

Porque debe verse en todas las pantallas. Al ponerlo en la plantilla raíz, fuera del <router-outlet />, se muestra siempre sin repetirlo ni olvidarlo en ninguna página.


Módulo C6: Iconos SVG con data binding

El problema que resuelve

Los iconos del menú son SVG escritos directamente en la plantilla. Se ven bien, pero tienen tamaño fijo (width="24" height="24") repetido en cada icono. Si mañana se quiere uno de 32 píxeles, hay que cambiar muchos sitios. Lo natural es guardar el tamaño en una variable de la clase y enlazarlo.

La regla: [attr.nombre]

En un SVG, los atributos se enlazan con [attr.nombre], no con [nombre]:

// top-menu.component.ts
export class TopMenuComponent {
  private readonly iconGrid = 24                       // cuadrícula del dibujo
  viewBox = `0 0 ${this.iconGrid} ${this.iconGrid}`    // fijo: depende del dibujo
  iconSize = 22                                         // tamaño en pantalla
}
<svg
  xmlns="http://www.w3.org/2000/svg"
  [attr.width]="iconSize"
  [attr.height]="iconSize"
  [attr.viewBox]="viewBox"
>
  <path d="M0 0h24v24H0z" fill="none" />
  <path fill="none" stroke="currentColor" d="..." />
</svg>
Qué se enlaza Forma
Un atributo del SVG (width, height, viewBox, fill, d...) [attr.width]="valor"
Una clase [class.activo]="condicion" o [class]="expresion"
Un estilo [style.fill]="color"
Texto dentro del SVG {{ valor }}
Un evento (click)="metodo()"

Por qué no [width]

Con corchetes simples, Angular enlaza una propiedad del elemento en el DOM. En un <svg>, width no es una propiedad simple sino un objeto especial del navegador, y Angular no la reconoce: la plantilla no compila. [attr.width], en cambio, escribe directamente el atributo HTML, que es lo que se necesita. Esto se comprobó compilando las dos formas en el proyecto.

El viewBox y el tamaño

Son dos cosas distintas, y mezclarlas es el error más común con iconos:

  • viewBox es el sistema de coordenadas del dibujo: la cuadrícula en la que se trazó el icono (0 0 24 24: un lienzo de 24 por 24 unidades).
  • width y height son el tamaño en pantalla: escalan ese lienzo.
viewBox  →  depende del DIBUJO                  (24: no cambia)
width    →  depende de DÓNDE SE MUESTRA         (iconSize: sí cambia)

El error: derivar el viewBox del tamaño

iconSize = 22
viewBox = `0 0 ${this.iconSize} ${this.iconSize}`   // 0 0 22 22  ✗

Parece lógico (el icono mide 22, el lienzo mide 22), pero el dibujo sigue trazado para 24 unidades. Con viewBox="0 0 22 22", las dos últimas unidades quedan fuera del lienzo y se recortan; y si el tamaño sube a 48, el viewBox pasa a 0 0 48 48 y el icono no crece: queda del mismo tamaño dentro de una caja mayor.

La solución: dos variables

export class TopMenuComponent {
  // La cuadrícula en la que se dibujaron los iconos. No cambia con el tamaño.
  private readonly iconGrid = 24
  viewBox = `0 0 ${this.iconGrid} ${this.iconGrid}`

  // Tamaño en pantalla. Cambiar este valor redimensiona todos los iconos.
  iconSize = 22
}
<svg [attr.width]="iconSize" [attr.height]="iconSize" [attr.viewBox]="viewBox">

La plantilla no cambia: sigue enlazando las tres propiedades. Lo que cambia es de dónde sale cada una. Ahora, iconSize puede valer 16, 22 o 48 y el dibujo siempre se escala completo, sin recortes.

Cómo se comprobó

Midiendo el dibujo real en el navegador, con el mismo icono:

Caja viewBox Dibujo resultante
24 px 0 0 24 24 23 px (referencia)
48 px 0 0 24 24 (fijo) 46 px: se amplía al doble
48 px 0 0 48 48 (derivado) 23 px: no crece
22 px 0 0 24 24 (solución) 21 px: escala 22/24

El orden de los campos de la clase

private readonly iconGrid = 24
viewBox = `0 0 ${this.iconGrid} ${this.iconGrid}`   // usa iconGrid

Los campos de una clase se inicializan en orden. viewBox usa iconGrid, así que debe ir después. Si se escribieran al revés, iconGrid valdría undefined en ese momento y TypeScript lo marca.

Si iconSize pasa a ser una entrada (input()) o una señal, el viewBox no tiene por qué cambiar: sólo width y height leen la señal ([attr.width]="iconSize()"). Un computed para el viewBox sólo tiene sentido si lo que varía es la cuadrícula original del icono (16, 20 o 24), no el tamaño con que se muestra:

iconGrid = signal(24)                        // cuadrícula en la que se dibujó
iconSize = signal(32)                        // tamaño en pantalla
viewBox = computed(() => `0 0 ${this.iconGrid()} ${this.iconGrid()}`)

La regla para recordarlo: el viewBox depende del dibujo; width y height, de dónde se muestra.

Color: currentColor

<path fill="none" stroke="currentColor" d="..." />

currentColor significa "el color del texto de este elemento". El icono toma el color de su contenedor: dentro del menú de daisyUI cambia con el tema y con el estado (activo, hover) sin escribir ningún color.

De dónde salen los iconos

Los de este proyecto vienen de Iconify, un catálogo de conjuntos de iconos. Desde ahí se copia el SVG y se pega en la plantilla, o se importa como componente. Pegado en la plantilla es lo más sencillo, y permite este tipo de enlace.

Instructivo: un icono SVG con tamaño dinámico

  1. Copia el SVG con su viewBox original y déjalo así: es la cuadrícula del dibujo (iconGrid).
  2. En la clase, una propiedad con el tamaño en pantalla (iconSize = 22), distinta de la cuadrícula.
  3. En el <svg>, [attr.width]="iconSize" y [attr.height]="iconSize".
  4. En los trazos, stroke="currentColor" (o fill="currentColor") para heredar el color.
  5. Para algo que cambia según datos: [attr.fill], [attr.d], [class.…] o [style.…].
  6. Si se repite mucho, un componente icon con input() para el nombre y el tamaño.

Errores comunes

  • [width] en vez de [attr.width]: la plantilla no compila.
  • Derivar el viewBox del tamaño (0 0 ${iconSize} ${iconSize}): con un tamaño menor que la cuadrícula recorta el icono, y con uno mayor no lo escala. El viewBox depende del dibujo; el tamaño es width y height.
  • Poner un color fijo en el trazo: el icono no sigue al tema. Usa currentColor.
  • Declarar viewBox antes que iconSize: iconSize aún no tiene valor.
  • Olvidar xmlns: al pegar un SVG suelto, conviene conservar su xmlns="http://www.w3.org/2000/svg".

Resumen

En un SVG se enlaza con [attr.nombre], porque Angular enlaza propiedades del DOM y las medidas del SVG son atributos. El tamaño se cambia con width y height, nunca con el viewBox, que define el lienzo. currentColor hace que el icono herede el color del texto y siga al tema.

Preguntas de entrevista

1. ¿Por qué [width] no funciona en un <svg> y [attr.width] sí?

Respuesta:

[width] enlaza una propiedad del elemento en el DOM, y en un <svg> esa propiedad no es un valor simple sino un objeto especial del navegador, que Angular no reconoce. [attr.width] enlaza el atributo, que es un texto, y eso sí se puede asignar. Es la regla general para cualquier atributo que no sea una propiedad.

2. ¿Cuál es la diferencia entre viewBox y width/height?

Respuesta:

viewBox define el sistema de coordenadas interno del dibujo (el lienzo); width y height definen el tamaño con que ese lienzo se muestra. Cambiando width y height el dibujo se escala sin deformarse; cambiar el viewBox cambia qué parte del lienzo se ve y puede recortar o desplazar el icono.

3. ¿Para qué sirve currentColor?

Respuesta:

Es un valor de CSS que equivale al color de texto del elemento. Usado en el stroke o fill de un SVG, el icono toma el mismo color que el texto que lo rodea, de modo que cambia con el tema, con el estado del botón o con una clase de color, sin definir colores en el SVG.

Clone this wiki locally