Skip to content

🧭 Clase 8 — Routing

📅 2026-07-18 · 🗂️ Proyecto: AppClase04 (destino en este repo: app-semana04) · 🎯 Routing (enrutamiento)

⬅️ Viene después de Clase 4, Clase 5, Clase 6 y Clase 7 (mismo día de clase).

Routing es el mecanismo de Angular para que la app cambie de "pantalla" (vista) según la URL, sin recargar la página completa (SPA — Single Page Application). El proyecto ya traía las piezas base desde ng new --routing: app.routes.ts y <router-outlet /> en app.html, pero vacíos — hoy se llenaron con rutas reales.

🎯 Qué aprendí

  • app.routes.ts define el mapa de rutas (Routes = array de objetos {path, component}), y provideRouter(routes) en app.config.ts es lo que activa el router en la app.
  • routerLink (en vez de href) + <router-outlet /> son la pareja mínima para navegar sin recargar la página: uno dispara la navegación, el otro pinta el componente según la URL.
  • Las rutas pueden tener un parámetro (:id) y, con withComponentInputBinding(), ese valor llega directo a un @Input() del componente, sin inyectar ActivatedRoute.
  • [routerLink]="['/productos', idProducto]" (array + property binding) arma la URL con un segmento variable, útil al recorrer una lista con @for.
  • Una ruta puede tener children (rutas hijas) que comparten un componente "layout" con su propio <router-outlet> anidado — así se arma una sección tipo dashboard.

🗺️ app.routes.ts: el mapa de rutas

ts
import { Routes } from '@angular/router';
import { Home } from './component/home/home';
import { Productos } from './component/productos/productos';
import { Salas } from './component/salas/salas';

export const routes: Routes = [
    {path: '', component: Home},
    {path: 'productos', component: Productos},
    {path: 'salas', component: Salas},
];
  • Cada objeto del array es una ruta: path es el pedazo de URL (sin / inicial), component es qué componente pintar cuando la URL coincide.
  • path: '' — la ruta raíz (localhost:4200/, sin nada después), muestra Home.
  • path: 'productos'localhost:4200/productos muestra Productos.

💡 Cada ruta es un objeto propio del array, sin ninguna llave { } extra envolviéndolas por fuera. routes es un array de objetos (Routes = Route[]), así que su forma siempre es [ {...}, {...}, {...} ] — nunca [ { {...}, {...}, {...} } ].

⚙️ app.config.ts: activar el router

ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes)
  ],
};
  • provideBrowserGlobalErrorListeners() — registra manejadores globales de errores del navegador (no es parte del routing, ya venía de fábrica).
  • provideRouter(routes) — esta es la pieza que activa el router: le entrega a Angular el mapa de rutas (routes, importado de app.routes.ts) para que sepa qué pintar según la URL.
html
<!-- app.html -->
<nav>
    <a routerLink="">Home</a> |
    <a routerLink="/productos">Productos</a> |
    <a routerLink="/salas">Salas</a>
</nav>

<router-outlet />
ts
// app.ts
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink, /* ...el resto de componentes... */],
  templateUrl: './app.html',
  styleUrl: './app.css',
})
  • routerLink en vez de href — navega sin recargar la página (cambia solo lo que hay dentro de <router-outlet />, no toda la app). Necesita RouterLink en imports.
  • <router-outlet /> — el "hueco" donde Angular pinta Home, Productos o Salas según la URL actual.

🧪 Tip de entrevista: ¿Por qué usar routerLink y no <a href="/productos">? → href fuerza una recarga completa de la página (pierde el estado de la SPA); routerLink deja que el Router de Angular intercepte el clic y solo actualiza <router-outlet />, mucho más rápido y sin perder el estado de otros componentes.

🔢 Rutas con parámetros (:id) + Component Input Binding

Segunda parte de Routing: pasar un dato variable en la propia URL (un id, un slug), y la forma nueva de Angular para recibirlo — directo en un @Input(), sin tener que inyectar ActivatedRoute y suscribirse a nada.

ts
// app.routes.ts
export const routes: Routes = [
    {path: '', component: Home},
    {path: 'productos', component: Productos},
    {path: 'productos/:id', component: Productos},   // 👈 ruta con parámetro
    {path: 'salas', component: Salas},
];

// /productos/5              -- id = "5"
// /productos/42             -- id = "42"
// /productos/zapatilla-nike -- id = "zapatilla-nike"
  • :id en el path es un segmento variable: cualquier texto ahí se captura como el parámetro id. Puede ser un número (5, 42) o un texto/slug (zapatilla-nike) — Angular no valida el tipo, siempre llega como string.
  • Se agregó una segunda ruta productos/:id además de productos (sin parámetro): así /productos (lista) y /productos/42 (un producto puntual) usan el mismo componente Productos, con o sin id.

⭐ Lo nuevo: withComponentInputBinding() (bindea el parámetro directo a un @Input)

ts
// app.config.ts
import { provideRouter, withComponentInputBinding } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes, withComponentInputBinding())   // 👈 la pieza nueva
  ],
};
ts
// productos.ts
export class Productos {
  @Input() id?: string;   // 👈 mismo nombre que ":id" en la ruta
}
html
<!-- productos.html -->
@if (id) {
  <p>Viendo el producto: <strong>{{ id }}</strong></p>
} @else {
  <p>Lista de productos (sin id en la URL)</p>
}
  • Antes (forma clásica, sin esta opción): había que inyectar ActivatedRoute en el constructor y suscribirse a route.paramMap para leer el id — más código, y había que acordarse de desuscribirse (ngOnDestroy) para no dejar la suscripción abierta.
  • Con withComponentInputBinding(): Angular detecta solo que la ruta tiene un parámetro :id, busca un @Input() con el mismo nombre (id) en el componente de esa ruta, y se lo asigna automáticamente. Cero código de "plomería" — el parámetro llega como si fuera un @Input() normal, igual que [nombre] en bienvenida.ts.
  • @Input() id?: string — con ? (opcional) porque /productos (sin :id) también usa este mismo componente, y ahí id llega undefined. El @if (id) del template decide qué mostrar según si hay id o no.

📝 En la captura de clase se escribió @Input() id!:String — dos detalles a corregir: String (mayúscula, el objeto envoltorio de JS) debería ser string (minúscula, el tipo primitivo — mismo error que ya salió antes con enviarACocina(nombrePlato: String) en 02-Conceptos.md). Y el operador ! (non-null assertion, "confío en que esto nunca es null") no aplica bien aquí porque id sí puede no llegar (cuando se entra por /productos sin parámetro) — por eso se usó ? (opcional) en vez de !.

🧪 Tip de entrevista: ¿Qué hace withComponentInputBinding()? → Le dice al router que mapee automáticamente los parámetros de ruta (:id, query params, y datos de resolve) a los @Input() del componente enrutado que tengan el mismo nombre, sin tener que inyectar ActivatedRoute manualmente.

Hasta ahora routerLink se usó con un texto fijo (routerLink="/productos/42"). Cuando el segmento de la URL es una variable (p. ej., recorriendo una lista con @for), se usa la sintaxis de array con property binding [ ]:

html
<!-- productos.html -->
<ul>
  @for (idProducto of idsDisponibles; track idProducto) {
    <li><a [routerLink]="['/productos', idProducto]">Producto {{ idProducto }}</a></li>
  }
</ul>
ts
// productos.ts
idsDisponibles = ['5', '42', 'zapatilla-nike'];
  • [routerLink]="['/productos', idProducto]" — un array donde cada elemento es un segmento de la URL final. Angular los une con /: para idProducto = '42' arma /productos/42; para 'zapatilla-nike' arma /productos/zapatilla-nike.
  • Es el mismo routerLink de siempre, pero con corchetes [ ] (property binding) porque el valor ya no es un texto fijo, sino una expresión (el array), igual que cualquier otro [algo]="expresión".
  • Se combina con el @for: cada vuelta arma un link distinto según el idProducto de esa vuelta — así se genera un enlace por producto sin escribirlos a mano uno por uno (como sí se hizo antes con los 2 links fijos "Producto 42" / "Producto zapatilla-nike" en el <nav> de app.html).

🧪 Tip de entrevista: ¿Cuándo usar routerLink="texto fijo" vs [routerLink]="[...]"? → Texto fijo cuando la ruta no cambia (un menú de navegación fijo, como Home); array con binding cuando la ruta depende de un dato variable (el id de cada item al recorrer una lista con @for).

🗂️ Rutas hijas (children) con un layout

Tercera parte de Routing: una ruta puede tener sub-rutas (children), que se pintan dentro de un componente "layout" — un componente que tiene su propio <router-outlet> para sus hijos, además del <router-outlet> principal de App.

📌 Para este ejemplo, el profe generó los componentes directo en src/app/, sin la subcarpeta component/ que veníamos usando (ng g c dashBoardLayout en vez de ng g c component/dashBoardLayout). Son las dos formas válidas de organizar componentes — aquí se siguió la del profe para este ejemplo puntual.

ts
// app.routes.ts
import { DashBoardLayout } from './dash-board-layout/dash-board-layout';
import { DashBoardInicio } from './dash-board-inicio/dash-board-inicio';
import { DashBoardConfig } from './dash-board-config/dash-board-config';
import { DashBoardPagos } from './dash-board-pagos/dash-board-pagos';

export const routes: Routes = [
    // ...otras rutas...
    {
        path: 'dashboard', component: DashBoardLayout,
        children: [
            {path: '', component: DashBoardInicio},
            {path: 'configuracion', component: DashBoardConfig},
            {path: 'pagos', component: DashBoardPagos},
        ]
    },
];

📌 Se agregó una tercera sub-ruta (pagosDashBoardPagos) siguiendo el mismo patrón: se genera el componente (ng g c dashBoardPagos), se importa en app.routes.ts, y se agrega su link en el <nav> de dash-board-layout.html (/dashboard/pagos). Así se ve que agregar una sub-ruta nueva no toca nada más — ni el layout, ni las otras sub-rutas.

html
<!-- dash-board-layout.html -->
<h1>PANEL DE ADMINISTRACION</h1>

<nav>
    <a routerLink="/dashboard">Inicio</a>
    <br>
    <a routerLink="/dashboard/configuracion">Configuracion</a>
</nav>

<router-outlet />
  • children: [...] — las rutas de esta lista son relativas a path: 'dashboard': {path: ''} es /dashboard (a secas), {path: 'configuracion'} es /dashboard/configuracion.
  • DashBoardLayout necesita su propio <router-outlet> — sin él, Angular no tendría dónde pintar DashBoardInicio/DashBoardConfig. Es el mismo concepto que el <router-outlet> de App, pero anidado un nivel más adentro.
  • El layout también puede tener su propia navegación (el <nav> con "Inicio" / "Configuración") que solo aplica dentro de la sección /dashboard, separada del menú principal de App.

🗺️ Diagrama: los 2 router-outlet anidados

Diagrama del profe, con los dos "huecos" donde Angular pinta según la URL — uno dentro del otro:

app.html
  <router-outlet>  ───────────────▶  outlet #1


                                 DashBoardLayout

                                      │  <router-outlet>  ──▶  outlet #2
                                      │                         │
                                      │                         ▼
                                      │                  DashBoardInicio
                                      │                  o DashBoardConfig
                                      │                  (dentro de DashBoardLayout)
  • outlet #1 (el de app.html) decide qué página completa mostrar: Home, Productos, Salas, DashBoardLayout, etc., según la URL.
  • Cuando la URL empieza con /dashboard, outlet #1 pinta DashBoardLayout — y dentro de ese componente hay un segundo <router-outlet> (outlet #2) que decide, según el resto de la URL, si pintar DashBoardInicio (/dashboard), DashBoardConfig (/dashboard/configuracion) o DashBoardPagos (/dashboard/pagos).
  • Cada <router-outlet> es independiente: el de App no sabe nada del de DashBoardLayout, solo sabe que tiene que pintar DashBoardLayout completo (con su propio outlet adentro).

🧪 Tip de entrevista: ¿Para qué sirven las rutas hijas (children) en vez de rutas planas? → Para compartir un layout común (menú lateral, header de sección, etc.) entre varias sub-páginas relacionadas, sin repetirlo en cada componente — el layout se pinta una vez y solo cambia lo que hay dentro de su <router-outlet> propio.

💡 Si un componente ya está registrado como una ruta (tiene su path en app.routes.ts), no hace falta además ponerlo a mano en el HTML del padre (<app-dash-board-layout> fijo en app.html, por ejemplo) — o se navega a él por routing (a través del <router-outlet>), o se incrusta fijo como cualquier componente normal, pero no ambas cosas a la vez: si se hace fijo, se vería siempre, en cualquier página, sin importar la URL. En este proyecto se dejó solo por routing.

🔑 Una ruta nueva, un objeto propio

Al agregar una ruta más (por ejemplo intranet), va como su propio objeto, separado por coma dentro del array — nunca compartiendo llaves { } con otra ruta:

ts
{path: 'intranet', component: Intranet},
{
    path: 'dashboard', component: DashBoardLayout,
    children: [ ... ],
},

🧪 Tip de entrevista: ¿Qué pasa si un objeto JS/TS termina con dos pares con la misma clave (por ejemplo dos path)? → No es error de sintaxis; el motor se queda con el último valor asignado a esa clave, silenciosamente. Por eso conviene que cada ruta tenga siempre sus propias llaves { } — mezclarlas en un mismo objeto descarta la primera sin avisar.

🏋️ Ejercicios con solución

Ejercicio 1 — Agregar una sub-ruta hija nueva

Al dashboard (con DashBoardLayout, DashBoardInicio, DashBoardConfig y DashBoardPagos ya como hijas) agrégale una cuarta sub-ruta reportes, con su propio componente DashBoardReportes, siguiendo el mismo patrón que se usó para pagos: crear el componente, importarlo en app.routes.ts y sumarlo al array children.

Ver solución
ts
// app.routes.ts
import { DashBoardLayout } from './dash-board-layout/dash-board-layout';
import { DashBoardInicio } from './dash-board-inicio/dash-board-inicio';
import { DashBoardConfig } from './dash-board-config/dash-board-config';
import { DashBoardPagos } from './dash-board-pagos/dash-board-pagos';
import { DashBoardReportes } from './dash-board-reportes/dash-board-reportes';

export const routes: Routes = [
    // ...otras rutas...
    {
        path: 'dashboard', component: DashBoardLayout,
        children: [
            {path: '', component: DashBoardInicio},
            {path: 'configuracion', component: DashBoardConfig},
            {path: 'pagos', component: DashBoardPagos},
            {path: 'reportes', component: DashBoardReportes},   // 👈 nueva
        ]
    },
];
html
<!-- dash-board-layout.html: se agrega su link, sin tocar el resto -->
<nav>
    <a routerLink="/dashboard">Inicio</a>
    <br>
    <a routerLink="/dashboard/configuracion">Configuracion</a>
    <br>
    <a routerLink="/dashboard/reportes">Reportes</a>
</nav>

<router-outlet />

En productos.html hay una lista idsDisponibles = ['5', '42', 'zapatilla-nike'] pintada con @for. Si en vez de usar [routerLink]="['/productos', idProducto]" se hubiera escrito cada link a mano con routerLink fijo (como los del <nav> de app.html), ¿cómo quedaría, y por qué conviene la versión con array?

Ver solución
html
<!-- Versión "a mano" (NO escala, solo sirve si la lista es fija y corta) -->
<ul>
  <li><a routerLink="/productos/5">Producto 5</a></li>
  <li><a routerLink="/productos/42">Producto 42</a></li>
  <li><a routerLink="/productos/zapatilla-nike">Producto zapatilla-nike</a></li>
</ul>

<!-- Versión con array binding (la que se documentó en clase) -->
<ul>
  @for (idProducto of idsDisponibles; track idProducto) {
    <li><a [routerLink]="['/productos', idProducto]">Producto {{ idProducto }}</a></li>
  }
</ul>

La versión "a mano" obliga a escribir un <a> por cada id y a tocar el HTML cada vez que la lista cambia. La versión con [routerLink]="['/productos', idProducto]" arma la URL a partir de la variable idProducto de cada vuelta del @for — si idsDisponibles crece o cambia, el HTML no se toca.

❓ Preguntas y respuestas

1. ¿Por qué usar routerLink en vez de <a href="/productos">?

Porque href fuerza una recarga completa de la página (se pierde el estado de la SPA); routerLink deja que el Router de Angular intercepte el clic y solo actualice el <router-outlet />, sin recargar toda la app.

2. ¿Qué hace withComponentInputBinding()?

Le dice al router que mapee automáticamente los parámetros de ruta (:id, query params, datos de resolve) a los @Input() del componente enrutado que tengan el mismo nombre — sin tener que inyectar ActivatedRoute ni suscribirse a paramMap.

3. ¿Cuándo usar routerLink="texto fijo" en vez de [routerLink]="[...]"?

Texto fijo cuando la ruta no cambia (un menú de navegación fijo, como Home o Salas); array con binding cuando la ruta depende de un dato variable (el id de cada item al recorrer una lista con @for).

4. ¿Para qué sirven las rutas hijas (children) en vez de rutas planas?

Para compartir un layout común (como DashBoardLayout, con su menú y su propio <router-outlet>) entre varias sub-páginas relacionadas (DashBoardInicio, DashBoardConfig, DashBoardPagos), sin repetir el layout en cada componente.

❓ Pendiente

  • [ ] Navegación programática (Router.navigate()) — no vista todavía.
  • [ ] routerLinkActive (resaltar el link de la ruta activa) — no visto todavía.
  • [ ] Forma clásica con ActivatedRoute/paramMap, para comparar con withComponentInputBinding() — no vista todavía.
  • [ ] Componente artefactos (visto en el explorador del profe) — código aún no visto.

➡️ Query params, página 404 (wildcard) y Lazy Loading se vieron en una clase posterior (Clase 9) con un proyecto nuevo — ver routing-avanzado.md.

📎 Apuntes relacionados

  • 05-Estructura-proyecto.md — ya menciona app.routes.ts y <router-outlet/> como parte de la estructura base del proyecto (sin profundizar en routing todavía).
  • 02-Conceptos.md — se resumirá aquí también en cuanto haya código confirmado.
  • routing-avanzado.md — Clase 9: continuación con query params, página 404 (wildcard) y Lazy Loading, con un proyecto nuevo (app-routing).

➡️ Siguiente

Query params, wildcard y Lazy Loading — ver routing-avanzado.md (Clase 9).