Apariencia
🧭 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.tsdefine el mapa de rutas (Routes= array de objetos{path, component}), yprovideRouter(routes)enapp.config.tses lo que activa el router en la app.routerLink(en vez dehref) +<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, conwithComponentInputBinding(), ese valor llega directo a un@Input()del componente, sin inyectarActivatedRoute. [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:
pathes el pedazo de URL (sin/inicial),componentes qué componente pintar cuando la URL coincide. path: ''— la ruta raíz (localhost:4200/, sin nada después), muestraHome.path: 'productos'→localhost:4200/productosmuestraProductos.
💡 Cada ruta es un objeto propio del array, sin ninguna llave
{ }extra envolviéndolas por fuera.routeses 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 deapp.routes.ts) para que sepa qué pintar según la URL.
🧭 Navegación con routerLink + <router-outlet />
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',
})routerLinken vez dehref— navega sin recargar la página (cambia solo lo que hay dentro de<router-outlet />, no toda la app). NecesitaRouterLinkenimports.<router-outlet />— el "hueco" donde Angular pintaHome,ProductosoSalassegún la URL actual.
🧪 Tip de entrevista: ¿Por qué usar
routerLinky no<a href="/productos">? →hreffuerza una recarga completa de la página (pierde el estado de la SPA);routerLinkdeja 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":iden elpathes un segmento variable: cualquier texto ahí se captura como el parámetroid. Puede ser un número (5,42) o un texto/slug (zapatilla-nike) — Angular no valida el tipo, siempre llega comostring.- Se agregó una segunda ruta
productos/:idademás deproductos(sin parámetro): así/productos(lista) y/productos/42(un producto puntual) usan el mismo componenteProductos, con o sinid.
⭐ 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
ActivatedRouteen el constructor y suscribirse aroute.paramMappara leer elid— 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]enbienvenida.ts. @Input() id?: string— con?(opcional) porque/productos(sin:id) también usa este mismo componente, y ahíidllegaundefined. El@if (id)del template decide qué mostrar según si hayido 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 serstring(minúscula, el tipo primitivo — mismo error que ya salió antes conenviarACocina(nombrePlato: String)en02-Conceptos.md). Y el operador!(non-null assertion, "confío en que esto nunca es null") no aplica bien aquí porqueidsí puede no llegar (cuando se entra por/productossin 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 deresolve) a los@Input()del componente enrutado que tengan el mismo nombre, sin tener que inyectarActivatedRoutemanualmente.
🔗 [routerLink] con sintaxis de array (enlaces dinámicos)
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/: paraidProducto = '42'arma/productos/42; para'zapatilla-nike'arma/productos/zapatilla-nike.- Es el mismo
routerLinkde 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 elidProductode 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>deapp.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, comoHome); 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 subcarpetacomponent/que veníamos usando (ng g c dashBoardLayouten vez deng 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 (
pagos→DashBoardPagos) siguiendo el mismo patrón: se genera el componente (ng g c dashBoardPagos), se importa enapp.routes.ts, y se agrega su link en el<nav>dedash-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 apath: 'dashboard':{path: ''}es/dashboard(a secas),{path: 'configuracion'}es/dashboard/configuracion.DashBoardLayoutnecesita su propio<router-outlet>— sin él, Angular no tendría dónde pintarDashBoardInicio/DashBoardConfig. Es el mismo concepto que el<router-outlet>deApp, 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 deApp.
🗺️ 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 deapp.html) decide qué página completa mostrar:Home,Productos,Salas,DashBoardLayout, etc., según la URL.- Cuando la URL empieza con
/dashboard,outlet #1pintaDashBoardLayout— y dentro de ese componente hay un segundo<router-outlet>(outlet #2) que decide, según el resto de la URL, si pintarDashBoardInicio(/dashboard),DashBoardConfig(/dashboard/configuracion) oDashBoardPagos(/dashboard/pagos). - Cada
<router-outlet>es independiente: el deAppno sabe nada del deDashBoardLayout, solo sabe que tiene que pintarDashBoardLayoutcompleto (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
pathenapp.routes.ts), no hace falta además ponerlo a mano en el HTML del padre (<app-dash-board-layout>fijo enapp.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 />Ejercicio 2 — Convertir un routerLink fijo en array binding
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
hreffuerza una recarga completa de la página (se pierde el estado de la SPA);routerLinkdeja 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 deresolve) a los@Input()del componente enrutado que tengan el mismo nombre — sin tener que inyectarActivatedRouteni suscribirse aparamMap.
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
HomeoSalas); 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 conwithComponentInputBinding()— 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 mencionaapp.routes.tsy<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).