Apariencia
🚦 Clase 9 — Routing avanzado (query params, wildcard, lazy loading)
📅 2026-07-25 · 🗂️ Proyecto:
app-routing(destino en este repo:02-Ejercicios/app-routing) · 🎯 Profundizar Routing: query params, página 404 (wildcard), rutas hijas (dashboard) y Lazy Loading.⬅️ Viene después de Clase 8 (Routing, 2026-07-18).
🎯 Qué aprendí
- Query params (
[queryParams]): a diferencia de un parámetro de ruta (:id), son información extra después del?en la URL, sin que la ruta lo declare — se leen igual que:id, con@Input()+withComponentInputBinding(). - Página 404 con la ruta wildcard (
path: '**'), siempre al final del array de rutas para no "tapar" a las demás. - Rutas hijas con
dashboard(children: [...]) y la diferencia entrerouterLinkrelativo ("perfil") y absoluto ("/perfil") dentro de una ruta anidada. - Route Guards:
CanActivate(authGuard, protege/dashboardantes de entrar) yCanDeactivate(puedeSalirGuard, confirma antes de salir de/editar-perfilcon cambios sin guardar) — y su límite frente awindow:beforeunload. inject()como forma funcional de pedir dependencias (Auth,Router) dentro de un guard, que es una función suelta sinconstructor.- Lazy Loading (
loadComponent/loadChildren): cargar el código de una ruta solo cuando el usuario navega a ella, confirmado conng build(chunks separados).
El profe retomó Routing en una clase nueva (Clase 9) con un proyecto distinto y más simple (app-routing, en su máquina) para cubrir temas que no se vieron en la Clase 8: query params, página 404 (ruta wildcard) y Lazy Loading. Como el profe no sube este proyecto al repo del curso, se recreó una versión propia en 02-Ejercicios/app-routing para poder correr y practicar en paralelo (ver sección correspondiente más abajo).
🆕 Proyecto de ejercicios en vivo: app-routing
Estructura de componentes vista en el explorador del profe:
contacto,dashboard(con sub-componentesperfilyajustes),inicio,pagina-no-encontrada,producto(singular, con:id),productos(plural, listado).
html
<!-- inicio.html -->
<h1>Bienvenido a la tienda</h1>
<a [routerLink]="['/producto', 5]">Ver Producto 5</a> <br>
<a [routerLink]="['/producto', 12]">Ver Producto 12</a> <br>
<a [routerLink]="['/productos']" [queryParams]="{categoria: 'ropa'}">Ver Ropa</a>
<!-- /producto/12 http://localhost:4200/productos?categoria=ropa -->- Primeros dos links: mismo patrón de array en
[routerLink]ya visto enapp-semana04(['/producto', 5]→/producto/5), aquí con la ruta en singularproducto/:iden vez deproductos/:id. - Tercer link (
/productos+[queryParams]): a diferencia de:id(que es parte de la ruta,path: 'producto/:id'), un query param es información extra que va después del?en la URL, sin que la ruta lo tenga que declarar.[queryParams]recibe un objeto ({ clave: valor }) con property binding[ ]; Angular lo convierte en?clave=valory lo pega a la URL que arma[routerLink]. [routerLink]="['/productos']"+[queryParams]="{categoria: 'ropa'}"→http://localhost:4200/productos?categoria=ropa. La ruta sigue siendo/productos(la misma para cualquier categoría); es la vista destino (Productos) la que puede leercategoriade la URL para filtrar qué mostrar.
📝 En una captura anterior el valor era
{categoria: 'ropa5'}— el profe lo corrigió a'ropa'(coincide con el comentario?categoria=ropay con lo que se ve luego en el navegador: "Categoria filtrada : ropa").
🖥️ Resultado en el navegador (lado de lectura)
Al hacer clic en "Ver Ropa", la vista Productos muestra el valor leído de la URL:
Inicio
Contacto
Categoria filtrada : ropa- Confirma que el query param sí llega al componente
Productosy se usa para filtrar/mostrar algo en pantalla — pero todavía no se vio el código que lo lee (si usaActivatedRoute.queryParamMap, uninput()de query param, o alguna otra forma). Se completa esta sección en cuanto aparezcaproducto.ts/productos.ts.
En una vista posterior de inicio.html en el navegador del profe, la pantalla de Inicio se ve así (confirma el <nav> de app.html con Inicio/Contacto arriba, y el tercer link ahora dice "Ver Laptos" en vez de "Ver Ropa"):
Inicio
Contacto
Bienvenido a la tienda
Ver Producto 5
Ver Producto 12
Ver Laptos📝 "Ver Laptos" — probablemente el profe cambió el texto/categoría del tercer link (de
ropaa algo comolaptops) y quedó con una errata (falta la segunda "p"). Falta ver elinicio.htmlactualizado para confirmar el nuevo valor exacto de[queryParams]y corregir el texto en el proyecto de práctica.
📞 contacto.html
Navegando a /contacto (vía el link "Contacto" del <nav> de app.html), se ve:
Inicio
Contacto
Contáctanos
Escribenos a contacto@gmail.com.pe💡 Un correo (
contacto@gmail.com.pe) escrito directo en el template como texto plano — en Angular, el signo@a inicio de un bloque (@if,@for...) es sintaxis de control flow, pero dentro de texto normal no genera conflicto; solo hay que tener cuidado si el@queda justo donde Angular podría interpretarlo como el inicio de un bloque. En el proyecto de práctica se escapó como@por seguridad, aunque en medio de una oración normalmente no hace falta.
🛠️ Proyecto de práctica local: 02-Ejercicios/app-routing
Como el profe aún no sube su
app-routingal repo, se recreó una versión propia (mismos nombres de componentes:inicio,contacto,producto,productos,dashboard,pagina-no-encontrada) para poder correr y tocar el ejercicio ya mismo. No es una copia del código del profe — es una reconstrucción con lo documentado arriba, para practicar en paralelo. Se ajusta/corrige en cuanto se vea el código real.
app.routes.ts— las rutas (incluyepath: '**'para la página 404, ver siguiente sección) yproductosreutilizado como enapp-semana04, másdashboardconchildren: [perfil, ajustes](código real del profe, ver abajo).dashboard.html—<nav>+<router-outlet>, con ambos links corregidos a relativos (routerLink="perfil"/routerLink="ajustes", sin/), a diferencia del/perfilabsoluto que quedó en la captura del profe (ver el ⚠️ más abajo).app.config.ts—provideRouter(routes, withComponentInputBinding()).producto.ts—@Input() id?: string(igual queProductosenapp-semana04).productos.ts—@Input() categoria?: string: se probó quewithComponentInputBinding()no solo bindea parámetros de ruta (:id), también bindea query params al@Input()con el mismo nombre — sin tocarActivatedRoutepara nada. Es la forma más simple de leer?categoria=ropay explica el "Categoria filtrada : ropa" del navegador.
$ cd 02-Ejercicios/app-routing
$ npm start // → http://localhost:4200🧪 Tip de entrevista: ¿
withComponentInputBinding()solo sirve para:iden la ruta? → No — también bindea query params y datos deresolvea@Input()s del mismo nombre. Por esocategoriaen la URL (?categoria=ropa) puede llegar directo a@Input() categoriasin inyectarActivatedRoute.
🧪 Tip de entrevista: ¿Diferencia entre un parámetro de ruta (
:id) y un query param (?categoria=...)? → El parámetro de ruta es parte de la URL fija (la ruta declarapath: 'producto/:id', y sin ese segmento la ruta ni siquiera matchea); el query param es opcional y no forma parte delpath— sirve para filtros, orden, paginación, etc., y la misma ruta funciona con o sin él.
📝 Este proyecto vive solo en la máquina del profe (
~/angular/app-routing, terminal Git Bash de Windows) — no hay código propio que copiar todavía, solo transcripción de lo mostrado en pantalla. La carpetapagina-no-encontrada/vista en su explorador se reconstruyó en el proyecto de práctica de abajo, con una ruta wildcard.
🗂️ dashboard con children: perfil y ajustes
Código real visto en app.routes.ts del profe (a diferencia de la ruta wildcard más abajo, que es una adición propia para practicar):
ts
// app.routes.ts
import { Routes } from '@angular/router';
import { Inicio } from './inicio/inicio';
import { Contacto } from './contacto/contacto';
import { Producto } from './producto/producto';
import { Productos } from './productos/productos';
import { Dashboard } from './dashboard/dashboard';
import { Perfil } from './dashboard/perfil/perfil';
import { Ajustes } from './dashboard/ajustes/ajustes';
export const routes: Routes = [
{ path: '', component: Inicio },
{ path: 'contacto', component: Contacto },
{ path: 'producto/:id', component: Producto },
{ path: 'productos', component: Productos },
{
path: 'dashboard', component: Dashboard,
children: [
{ path: 'perfil', component: Perfil },
{ path: 'ajustes', component: Ajustes },
]
},
];- Mismo patrón de rutas hijas ya documentado con
app-semana04(DashBoardLayout/DashBoardInicio/DashBoardConfig):Dashboardnecesita su propio<router-outlet>para pintarPerfiloAjustessegún la URL (/dashboard/perfilo/dashboard/ajustes). - A diferencia del ejemplo de
app-semana04, aquí no hay una ruta{path: ''}dentro dechildren— entrar a/dashboarda secas no matchea ningún hijo (queda en blanco dentro del layout, salvo que el profe agregue una en el siguiente paso). - El explorador de archivos muestra
perfil/yajustes/como subcarpetas dentro dedashboard/(no en la raíz deapp/, ni en una carpetacomponent/aparte) — otra forma válida de agrupar componentes que pertenecen a una sola sección.
dashboard.html / dashboard.ts — el layout con su propio <router-outlet>
ts
// dashboard.ts
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-dashboard',
imports: [RouterLink, RouterOutlet],
templateUrl: './dashboard.html',
styleUrl: './dashboard.css',
})
export class Dashboard {}html
<!-- dashboard.html -->
<nav>
<a routerLink="/perfil">Perfil</a>
<a routerLink="ajustes">Ajustes</a>
</nav>
<router-outlet></router-outlet>⚠️ Error real: el link de "Perfil" quedó con
/perfil(con/inicial → ruta absoluta, resuelve desde la raíz del sitio:http://localhost:4200/perfil, que no existe como ruta — cae en la página 404). El de "Ajustes" quedó sin/(ajustes, ruta relativa a donde está el link) → como este<nav>vive dentro deDashboard(montado en/dashboard), resuelve correctamente a/dashboard/ajustes. Corrección: ambos deberían ser relativos,routerLink="perfil"yrouterLink="ajustes"(sin/), para que los dos apunten dentro de/dashboard/....
🧪 Tip de entrevista: ¿Qué diferencia hay entre
routerLink="perfil"yrouterLink="/perfil"? → Sin/es una ruta relativa al segmento actual de la URL (útil dentro dechildren, para no repetir el prefijo del padre); con/inicial es absoluta, siempre parte desde la raíz del sitio — si el componente vive anidado (comoDashboard), un link absoluto a un hijo suyo (/perfilen vez de/dashboard/perfil) apunta a una ruta que no existe.
🐛 Error propio (proyecto de práctica): links pegados sin espacio
Al escribir varios <a> en líneas separadas dentro de un <nav> (tanto en app.html como en dashboard.html), el navegador los mostró todos pegados: InicioContactoProductosDashboard, sin espacio entre ellos.
html
<!-- ❌ así se ven pegados -->
<nav>
<a routerLink="">Inicio</a>
<a routerLink="/contacto">Contacto</a>
</nav>- Causa: Angular, al compilar la plantilla, elimina el espacio en blanco "insignificante" entre elementos (saltos de línea + indentación no cuentan como texto real) — es una optimización por defecto (equivalente a
preserveWhitespaces: false). El salto de línea que se ve en el editor no sobrevive a la compilación. - Corrección: agregar un separador explícito entre los links (texto,
|, o un<br>), como ya se hacía enapp-semana04:
html
<!-- ✅ con separador explícito -->
<nav>
<a routerLink="">Inicio</a> |
<a routerLink="/contacto">Contacto</a> |
<a routerLink="/productos">Productos</a> |
<a routerLink="/dashboard">Dashboard</a>
</nav>🧪 Tip de entrevista: ¿Por qué elementos en líneas separadas en el HTML pueden aparecer pegados en el navegador con Angular? → Porque Angular descarta el espacio en blanco insignificante (saltos de línea/indentación) al compilar la plantilla — el salto de línea del editor no es un espacio real para el HTML final. Hay que poner un separador explícito (texto,
|,<br>) si se quiere espacio visible entre elementos inline.
🃏 Ruta wildcard (**) para página 404
En el proyecto de práctica se agregó, al final del array de rutas:
ts
// app.routes.ts
export const routes: Routes = [
{ path: '', component: Inicio },
// ...el resto de rutas...
{ path: '**', component: PaginaNoEncontrada }, // 👈 siempre al final
];path: '**'— comodín que matchea cualquier URL que no haya sido capturada por ninguna ruta anterior (/lo-que-sea,/asdf123, etc.).- Debe ir al final del array — el router prueba las rutas en orden y se queda con la primera que matchee; si
'**'fuera la primera, capturaría todo y ninguna otra ruta se alcanzaría a evaluar nunca.
🧪 Tip de entrevista: ¿Por qué la ruta
**siempre va al final? → Angular Router evalúa las rutas en el orden del array y usa la primera coincidencia;**matchea cualquier cosa, así que si estuviera antes "taparía" a todas las rutas siguientes, haciendo que nunca se muestren.
🗺️ Diagrama: el árbol de rutas completo (proyecto de práctica)
Resumen visual de todas las rutas de 02-Ejercicios/app-routing tal como quedaron al cierre de esta clase — perfil/ajustes siguen marcadas como pendientes porque están comentadas en el app.routes.ts real (ver sección de dashboard más arriba):

'**'siempre al final — se ve literalmente como el último hijo deroutesen el diagrama, confirmando la regla de la sección anterior.perfil/ajustesen naranja punteado — no es que no existan: los componentes están creados, pero la líneachildren: [...]dedashboardquedó comentada en el código mientras se probaba el guardcanActivate. El diagrama lo marca como pendiente en vez de mostrarlo como si ya estuviera activo.
⚡ Lazy Loading (carga perezosa)
El profe explicó el tema y lo aplicó en vivo a la ruta
contactodeapp-routing. Hasta ese momento, todos los componentes de las rutas (Inicio,Contacto,Producto,Productos,Dashboard...) se importaban arriba deapp.routes.tscon unimportnormal — eso significa que todos viajan juntos en el mismo bundle (main.js) desde el primer segundo, aunque el usuario solo visite/.
Lazy Loading es cargar el código de una ruta solo cuando el usuario navega a ella, en vez de meterlo todo en el paquete inicial de la aplicación.
🎯 Para qué sirve
- App más rápida al arrancar: el navegador descarga menos JavaScript en la primera carga (
main.jsmás chico) — solo lo que se necesita para pintar la ruta inicial (/). - El código de rutas que el usuario puede que nunca visite (p. ej.
Dashboard, si es un panel de admin que solo algunos usuarios abren) no se descarga hasta que hace falta. - Es más notorio mientras más rutas/páginas tenga la app — en un proyecto de 4-5 componentes como este no se nota mucho, pero en una app real con decenas de páginas es la diferencia entre un
main.jsde varios MB o uno pequeño.
🆚 Eager (actual) vs Lazy — comparación
| Eager loading (lo que hay ahora) | Lazy loading | |
|---|---|---|
| Import | import { Producto } from './producto/producto' arriba del archivo | Ningún import estático — se usa import() dinámico dentro de la ruta |
| En la ruta | { path: 'producto/:id', component: Producto } | { path: 'producto/:id', loadComponent: () => import('./producto/producto').then(m => m.Producto) } |
| Cuándo se descarga el código | Todo junto, al cargar la app (en main.js) | Solo al navegar a esa ruta (un chunk .js aparte) |
| Verificación | Un solo main.js grande en ng build | Varios archivos "lazy chunk" además de main.js |
🧩 Sintaxis moderna: loadComponent (standalone)
Código real que puso el profe (convirtiendo contacto de eager a lazy):
ts
// app.routes.ts
{ path: 'contacto', loadComponent: () => import('./contacto/contacto').then(m => m.Contacto) },- Antes:
import { Contacto } from './contacto/contacto';arriba del archivo +{ path: 'contacto', component: Contacto }— eager, viaja enmain.js. - Ahora: sin import estático de
Contactoenapp.routes.ts— elimport()dentro deloadComponentes dinámico, y el bundler lo separa en su propio chunk.
Aplicado en el proyecto de práctica (02-Ejercicios/app-routing) y confirmado con ng build:
$ ng build
Initial chunk files | Names | Raw size
chunk-WGOAFEWR.js | - | 113.40 kB
main-GRYY4BSK.js | main | 96.24 kB
Lazy chunk files | Names | Raw size
chunk-ZQAZA6DJ.js | contacto | 361 bytes- El
main.jsinicial bajó frente al build anterior (todo eager) porqueContactoya no viaja ahí — quedó en su propio lazy chunk (contacto, 361 bytes) que solo se descarga cuando el usuario navega a/contacto. - Así se ve en la práctica lo que antes era solo teoría: la tabla comparativa de arriba (eager vs lazy) — el build es la prueba.
🌐 Confirmación en el navegador (pestaña Network)
En el profe (con ng serve), la pestaña Network del DevTools mostró peticiones como:
component?c=src/app/dashboard/perfil/perfil.ts...
component?c=src/app/dashboard/ajustes/ajustes.ts...
component?c=src/app/pagina-no-encontrada/pagina-no-encontrada.ts...
chunk-TEGXLTFG.js
component?c=src/app/contacto/contacto.ts... 👈 aparece al entrar a /contacto- Confirma lo que se explicó arriba: el archivo de
contactono se pide de entrada, aparece en la lista justo cuando se navega a esa ruta — "trae contacto solo cuando lo necesito". - Matiz importante: estas peticiones
component?c=...son el dev server de Angular (ng serve, basado en Vite/esbuild) sirviendo cada componente por separado bajo demanda — esto pasa en modo desarrollo para cualquier componente (eager o lazy) a medida que el navegador los va necesitando para pintar la ruta activa, no es exclusivo deloadComponent. Por eso también aparecenperfil/ajustes(rutas eager, sinloadComponent) en la lista: se pidieron cuando se navegó a/dashboard/perfily/dashboard/ajustes, no porque sean lazy. - La prueba definitiva de que
contactoes lazy (y las otras no) es la deng buildde arriba: ahí sí se ve la diferencia real entre quedar en el bundle inicial (perfil/ajustes/producto, dentro demain.js) vs quedar en su propio chunk (contacto, solo se genera porque tieneloadComponent). El Network tab en desarrollo es más difícil de usar para diferenciar eager de lazy porque Vite sirve módulos on-demand para ambos casos.
🧪 Tip de entrevista: ¿Sirve la pestaña Network en
ng servepara confirmar lazy loading? → Con matices: en desarrollo (Vite/esbuild dev server) todos los componentes se piden bajo demanda según lo que el navegador va necesitando, sea la ruta eager o lazy — no es una prueba concluyente. La forma confiable es mirar el resultado deng build(producción): ahí las rutas lazy generan chunks separados aparte demain.js, y las eager quedan dentro del bundle inicial.
Ejemplo de cómo se vería si se extendiera el mismo patrón al resto de rutas (no aplicado todavía, solo ilustrativo):
ts
// app.routes.ts — ejemplo, no implementado aún
import { Routes } from '@angular/router';
import { Inicio } from './inicio/inicio'; // Inicio sigue eager: es la home, se ve siempre
export const routes: Routes = [
{ path: '', component: Inicio },
{ path: 'contacto', loadComponent: () => import('./contacto/contacto').then(m => m.Contacto) },
{
path: 'producto/:id',
loadComponent: () => import('./producto/producto').then(m => m.Producto),
},
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard').then(m => m.Dashboard),
children: [
{ path: 'perfil', loadComponent: () => import('./dashboard/perfil/perfil').then(m => m.Perfil) },
{ path: 'ajustes', loadComponent: () => import('./dashboard/ajustes/ajustes').then(m => m.Ajustes) },
],
},
];loadComponentreemplaza acomponent— en vez de una referencia directa a la clase (que obliga a importarla arriba), recibe una función que retorna unaPromisecon el import dinámico (import('./ruta')).- El
.then(m => m.Producto)saca la claseProductodel módulo que retorna el import dinámico (mes el archivo completo,m.Productoes la clase exportada). - Angular/el bundler (esbuild/Vite en Angular CLI) detecta estos
import()dinámicos y genera un chunk separado para cada uno — ese archivo solo se descarga cuando el usuario efectivamente navega a esa ruta.
📦 loadChildren — para agrupar varias rutas hijas en un solo chunk
Cuando una sección tiene varias rutas hijas (como dashboard con perfil/ajustes), en vez de poner loadComponent en cada hija, se puede mover todo el grupo a su propio archivo de rutas y cargarlo de una sola vez con loadChildren:
ts
// dashboard/dashboard.routes.ts (archivo nuevo, dentro de la carpeta dashboard/)
import { Routes } from '@angular/router';
import { Perfil } from './perfil/perfil';
import { Ajustes } from './ajustes/ajustes';
export const DASHBOARD_ROUTES: Routes = [
{ path: 'perfil', component: Perfil },
{ path: 'ajustes', component: Ajustes },
];ts
// app.routes.ts
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard').then(m => m.Dashboard),
loadChildren: () => import('./dashboard/dashboard.routes').then(m => m.DASHBOARD_ROUTES),
},loadChildrencarga un array de rutas completo de forma perezosa — útil para no repetirloadComponenten cada hija cuando toda la sección se visita en conjunto.- Dentro de
dashboard.routes.tslos imports dePerfil/Ajustessí pueden ser estáticos (component:normal) porque ya están dentro del mismo chunk perezoso — la perezosidad ya la dio elloadChildrende afuera.
🧪 Tip de entrevista: ¿
loadComponentvsloadChildren? →loadComponentcarga un solo componente de forma perezosa para una ruta puntual;loadChildrencarga un conjunto de rutas (normalmente de una sección completa, como un módulo de feature) de una vez, agrupando varias rutas hijas en el mismo chunk.
🧪 Tip de entrevista: ¿Cómo se verifica que el lazy loading realmente funciona? → Con
ng build, revisando que aparezcan chunks separados además demain.js(uno por cadaimport()dinámico); o en el navegador, pestaña Network del DevTools, viendo que el.jsde esa ruta se descarga recién al navegar a ella, no al cargar la app.
📌
loadComponentya está confirmado y corriendo (rutacontacto, ver arriba). Pendiente: ver si el profe también muestraloadChildren(paradashboardcon sus hijasperfil/ajustes) — por ahora esa parte sigue siendo solo el ejemplo ilustrativo de arriba, no código confirmado.
🔐 Servicio Auth (signal) — preparando Route Guards
El profe creó un servicio nuevo (auth.ts) con un signal para guardar si el usuario está logueado — la pieza típica que luego se usa para proteger rutas (Route Guards: CanActivate), aunque el guard todavía no se vio.
ts
// auth.ts
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class Auth {
private logueado = signal(true); // para validar que el guard funciona, sin login real todavía
estaLogueado(): boolean {
return this.logueado();
}
iniciarSesion(): void {
this.logueado.set(true);
}
cerrarSesion(): void {
this.logueado.set(false);
}
}@Injectable({ providedIn: 'root' })— registra el servicio como singleton global: una sola instancia compartida por toda la app (cualquier componente que lo inyecte recibe la misma), sin tener que agregarlo a ningúnproviders:manualmente.private logueado = signal(true)— el estado real es privado; nada fuera de la clase puede hacerauth.logueado.set(...)directamente. Solo se puede leer víaestaLogueado()o cambiar vía los métodosiniciarSesion()/cerrarSesion()— un patrón de encapsulamiento: la clase controla cómo se modifica su propio estado. Arranca entrue(no enfalsecomo en la primera versión) para poder probar que el guard deja pasar sin tener que armar un login real todavía — es un valor temporal de desarrollo, no el comportamiento final (una app real arrancaría deslogueada).estaLogueado(): boolean— llama al signal como función (this.logueado()) para leer su valor actual y lo expone como un método normal (no como el signal en sí), así quien use el servicio no necesita saber que por dentro es un signal.iniciarSesion()/cerrarSesion()— cambian el valor con.set(true)/.set(false); son la única forma de mutarlogueadodesde afuera de la clase.
🧪 Tip de entrevista: ¿Por qué exponer
estaLogueado()como método en vez de dejar elsignalpúblico? → Encapsulamiento: si el signal fuera público, cualquiera podría hacerauth.logueado.set(true)sin pasar poriniciarSesion(), saltándose cualquier lógica futura (validar credenciales, llamar a una API, etc.) que se agregue ahí. Exponer solo métodos controla cómo se puede cambiar el estado.
🧪 Tip de entrevista: ¿Para qué se usaría normalmente un servicio así en routing? → Como base de un Route Guard (
CanActivateFn): antes de entrar a una ruta protegida (p. ej./dashboard), el guard inyectaAuthy llama aestaLogueado(); si esfalse, bloquea la navegación y redirige (p. ej. a/o a un login).
📌 Pendiente: no se vio todavía el
CanActivateFnque use este servicio para proteger una ruta — solo el servicio en sí. Se completa esta sección en cuanto aparezca el guard.
🛡️ ng g guard — generando el Route Guard
El profe corrió el generador de Angular CLI para crear el guard:
$ ng g guard auth
? Which type of guard would you like to create?
❯◉ CanActivate
◯ CanActivateChild
◯ CanDeactivate
◯ CanMatchEl CLI pregunta qué tipo de guard crear (con checkboxes, se puede elegir más de uno) — en la clase se dejó marcado solo CanActivate (el resaltado en la captura).
| Tipo de guard | Cuándo se ejecuta | Uso típico |
|---|---|---|
CanActivate | Antes de entrar a una ruta | Bloquear el acceso si el usuario no está logueado (el caso de Auth) |
CanActivateChild | Antes de entrar a una ruta hija (children) | Proteger todas las sub-rutas de una sección (p. ej. todo /dashboard/*) sin repetir el guard en cada hija |
CanDeactivate | Antes de salir de una ruta | Confirmar "¿Salir sin guardar?" si hay un formulario con cambios sin guardar |
CanMatch | Antes de que el router decida si esa ruta aplica | Similar a CanActivate, pero puede hacer que el router siga probando otras rutas si no matchea (útil combinado con lazy loading, para ni siquiera descargar el chunk si no hay permiso) |
- El CLI generó
auth-guard.tscon la funciónauthGuard: CanActivateFn.
🧪 Tip de entrevista: ¿Diferencia entre
CanActivateyCanMatch? → Ambos deciden si se puede entrar a una ruta, peroCanActivateactúa después de que el router ya decidió que esa ruta es la que matchea (si lo bloquea, la navegación simplemente falla);CanMatchactúa durante la búsqueda de la ruta — si retornafalse, el router puede seguir probando otras rutas con el mismopath(por ejemplo, mostrar una versión distinta de la misma URL según el rol del usuario) y evita cargar el chunk lazy de esa ruta si no hay permiso.
✅ auth-guard.ts completo, conectado a dashboard
ts
// auth-guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { Auth } from './auth';
export const authGuard: CanActivateFn = () => {
const authService = inject(Auth);
const router = inject(Router);
if (authService.estaLogueado()) {
return true;
}
router.navigate(['/']);
return false;
};ts
// app.routes.ts
import { authGuard } from './auth-guard';
export const routes: Routes = [
// ...otras rutas...
{ path: 'dashboard', component: Dashboard, canActivate: [authGuard] },
];inject(Auth)/inject(Router)—inject()es la forma funcional de pedir una dependencia (equivalente a ponerla en elconstructor), necesaria aquí porque un guardCanActivateFnes una función suelta, no una clase con constructor. Ver la explicación completa (qué es, desde qué versión, y por qué se recomienda como default desde Angular 17) en 02-Conceptos.md § inject().if (authService.estaLogueado())— si el signallogueadoestá entrue, el guard retornatruey el router deja continuar la navegación normalmente.- Si no está logueado:
router.navigate(['/'])manda al usuario a la home (no hay página de login en este proyecto todavía) y el guard retornafalse, bloqueando la navegación a/dashboard. canActivate: [authGuard]— un array de guards en la ruta; Angular los evalúa antes de activar esa ruta (y suschildren, salvo que además se agreguecanActivateChild). Conlogueadoenfalse(valor inicial del signal), entrar a/dashboarddirecto redirige a/— hay que llamar antes aauth.iniciarSesion()(p. ej. desde un botón "Login" en algún componente) para poder entrar.
🧪 Tip de entrevista: ¿Por qué
inject()en vez de un constructor en un guard funcional? → UnCanActivateFn(oCanActivate,CanMatch, etc. en su forma moderna) es una función, no una clase — no tieneconstructor.inject()permite pedir dependencias del contexto de inyección de Angular dentro de una función, siempre y cuando se llame en el momento correcto (durante la ejecución del guard, que Angular invoca dentro de un contexto de inyección válido).
📌 El signal
logueadoarranca ahora entrue(temporal, ver comentario en el código) para confirmar que el guard deja pasar sin armar todavía un login real. Falta un botón/acción que llame aauth.cerrarSesion()para probar también el otro camino: entrar a/dashboarddeslogueado → bloqueado, redirige a/.
🚪 Segundo guard: puede-salir (¿CanDeactivate?)
El profe generó un segundo guard, esta vez con un nombre que sugiere el patrón clásico de "confirmar antes de salir de una página con cambios sin guardar":
$ ng g guard puede-salir
? Which type of guard would you like to create?
◯ CanActivate
◯ CanActivateChild
❯◉ CanDeactivate
◯ CanMatch
CREATE src/app/puede-salir-guard.spec.ts (486 bytes)
CREATE src/app/puede-salir-guard.ts (134 bytes)Mismo menú de tipos que con auth (ver tabla más arriba), esta vez con CanDeactivate resaltado en la selección.
El archivo generado (reconstruido en el proyecto de práctica) trae un esqueleto genérico — el CLI arma CanActivateFn para cualquier tipo de guard, sin importar cuál se haya seleccionado (CanActivate, CanDeactivate, etc.), así que hay que ajustar el tipo a mano según el guard que realmente se quiera:
ts
// puede-salir-guard.ts
import { CanDeactivateFn } from '@angular/router';
export interface ComponenteConCambiosSinGuardar {
hayCambiosSinGuardar: () => boolean;
}
export const puedeSalirGuard: CanDeactivateFn<ComponenteConCambiosSinGuardar> = (component) => {
if (component.hayCambiosSinGuardar()) {
return confirm('Tienes cambios sin guardar, ¿Seguro que deseas salir?');
}
return true;
};CanDeactivateFn<T>(noCanActivateFn) — el tipo correcto para un guard que decide si se puede salir de una ruta;Tes la interfaz que describe el componente que se está por abandonar.ComponenteConCambiosSinGuardar— la interfaz declara el contrato que debe cumplir cualquier componente protegido por este guard: tener un métodohayCambiosSinGuardar()que el guard pueda llamar.- Firma distinta a
CanActivate: un guardCanDeactivaterecibe la instancia del componente como primer parámetro (component, para poder llamarcomponent.hayCambiosSinGuardar()), mientras queCanActivate/CanActivateFnrecibe(route, state)— la ruta activada y el estado del router, sin acceso al componente.
🧪 Tip de entrevista: ¿Para qué sirve específicamente
CanDeactivate(a diferencia deCanActivate)? → Para interceptar la salida de una ruta, típicamente para preguntar "¿Salir sin guardar los cambios?" en un formulario — necesita poder consultarle al componente (no solo a la ruta) si tiene cambios pendientes, por eso recibe la instancia del componente como parámetro, a diferencia deCanActivateque solo mira la ruta/URL.
🗺️ Diagrama: los dos guards en acción
Con los dos guards ya vistos (authGuard en canActivate y puedeSalirGuard en canDeactivate), el flujo completo de una navegación protegida, de entrada y de salida:

canActivate(arriba) — se dispara antes de entrar a/dashboard; siestaLogueado()esfalse, ni siquiera llega a pintarDashboard, redirige directo.canDeactivate(abajo) — se dispara al intentar salir de/editar-perfil; solo sihayCambiosSinGuardar()estrueaparece elconfirm()— si el usuario cancela, la navegación se cancela y se queda en la misma página.
🔌 Conectado: ruta editar-perfil
Se creó un componente nuevo, EditarPerfil, que implementa la interfaz del guard para poder usarlo de verdad:
ts
// editar-perfil.ts
import { Component, HostListener } from '@angular/core';
import { ComponenteConCambiosSinGuardar } from '../puede-salir-guard';
@Component({
selector: 'app-editar-perfil',
imports: [],
templateUrl: './editar-perfil.html',
styleUrl: './editar-perfil.css',
})
export class EditarPerfil implements ComponenteConCambiosSinGuardar {
formularioTocado = false;
hayCambiosSinGuardar(): boolean {
return this.formularioTocado;
}
// Cubre salir por FUERA de Angular: escribir otra URL, recargar o cerrar la
// pestaña. CanDeactivate no alcanza a correr ahí porque no es navegación del Router.
@HostListener('window:beforeunload', ['$event'])
avisarAntesDeSalir(event: BeforeUnloadEvent): void {
if (this.hayCambiosSinGuardar()) {
event.preventDefault();
}
}
}html
<!-- editar-perfil.html -->
<h2>Editar perfil</h2>
<label>
Nombre:
<input type="text" (input)="formularioTocado = true">
</label>
<p>¿Cambios sin guardar?: {{ formularioTocado }}</p>ts
// app.routes.ts
import { EditarPerfil } from './editar-perfil/editar-perfil';
import { puedeSalirGuard } from './puede-salir-guard';
export const routes: Routes = [
// ...otras rutas...
{ path: 'editar-perfil', component: EditarPerfil, canDeactivate: [puedeSalirGuard] },
];implements ComponenteConCambiosSinGuardar— TypeScript obliga aEditarPerfila tener el métodohayCambiosSinGuardar()que el guard necesita llamar; es el "contrato" entre el guard y cualquier componente que quiera usarlo.formularioTocado— se pone entruecon(input)="formularioTocado = true"en el<input>de Nombre — cualquier tecla que se escriba ahí marca el formulario como "con cambios sin guardar".canDeactivate: [puedeSalirGuard]— mismo patrón quecanActivate, pero para la salida: antes de dejar/editar-perfilpor cualquier otra ruta de Angular (clic en unrouterLink,Router.navigate()), Angular llama al guard con la instancia deEditarPerfil; sihayCambiosSinGuardar()estrue, aparece elconfirm()personalizado ("Tienes cambios sin guardar, ¿Seguro que deseas salir?").
⚠️ Un límite importante: CanDeactivate no cubre TODO tipo de salida
Al probar escribiendo otra URL directamente en la barra de direcciones (en vez de hacer clic en un link), el confirm() personalizado no salía. La razón: escribir una URL nueva y dar Enter es una navegación completa del navegador (recarga la página desde cero) — Angular Router ni se entera, así que CanDeactivate nunca llega a ejecutarse. CanDeactivate solo intercepta navegación dentro de la SPA (clics en routerLink, Router.navigate()).
Para cubrir también ese caso (URL manual, recargar la página, cerrar la pestaña) se necesita un mecanismo del navegador, no de Angular: el evento window:beforeunload, con @HostListener (ver el código de arriba).
CanDeactivate (Angular) | beforeunload (navegador) | |
|---|---|---|
| Cuándo actúa | Navegación dentro de la SPA (routerLink, Router.navigate()) | Cualquier salida de la página completa: URL nueva, recargar, cerrar pestaña/navegador |
| Mensaje del popup | Personalizable (confirm('tu texto')) | Genérico, fijo por el navegador — no se puede personalizar (por seguridad, desde hace años ningún navegador moderno deja poner texto propio) |
| Para qué sirve | Confirmar antes de cambiar de ruta dentro de la app | Confirmar antes de perder la pestaña/página por completo |
⚠️ El mensaje que muestra
beforeunloades el que decide el navegador (en Chrome: algo como "¿Salir del sitio? Es posible que los cambios que hiciste no se guarden."), no el texto que se le pase aconfirm()— es una limitación de todos los navegadores modernos, no un error del código.
🧪 Tip de entrevista: ¿
CanDeactivatees suficiente para avisar siempre antes de perder cambios sin guardar? → No — solo cubre navegación dentro del Router de Angular. Para cubrir recargar la página, cerrar la pestaña, o escribir otra URL, hace falta además un listener dewindow:beforeunload. Son mecanismos complementarios, no uno reemplaza al otro.
🏋️ Ejercicios con solución
Ejercicio 1 — Proteger /formulario con authGuard
En el proyecto de práctica, la ruta formulario (ContactoForm) está sin proteger:
ts
{ path: 'formulario', component: ContactoForm },Usando el mismo authGuard ya conectado a dashboard (canActivate: [authGuard]), protege también /formulario para que solo se pueda entrar si Auth.estaLogueado() devuelve true.
Ver solución
ts
// app.routes.ts
import { authGuard } from './auth-guard';
export const routes: Routes = [
// ...otras rutas...
{ path: 'formulario', component: ContactoForm, canActivate: [authGuard] },
// ...
];- Mismo patrón que
dashboard: un array de guards encanActivate. Angular llama aauthGuardantes de activar la ruta; siestaLogueado()esfalse, redirige a/y la navegación a/formulariose bloquea. - No hace falta tocar
auth-guard.ts— el guard ya es genérico (no depende de la ruta).
Ejercicio 2 — Hacer lazy la ruta /productos
Siguiendo el patrón ya aplicado a contacto (de component: Contacto a loadComponent: () => import(...).then(m => m.Contacto)), convierte productos de eager a lazy:
ts
// estado actual (eager)
{ path: 'productos', component: Productos },Ver solución
ts
// app.routes.ts
export const routes: Routes = [
// ...otras rutas...
{
path: 'productos',
loadComponent: () => import('./productos/productos').then(m => m.Productos),
},
// ...
];- Se quita el
import { Productos } from './productos/productos';estático de arriba del archivo (si ya no lo usa ninguna otra ruta) — el import ahora es dinámico, dentro de la función. - Se confirma con
ng build:Productosdebería pasar de estar dentro demain.jsa aparecer como un lazy chunk propio en la tabla "Lazy chunk files", igual que pasó concontacto(361 bytes).
Ejercicio 3 — Leer un query param nuevo (orden) en Productos
Siguiendo el patrón ya documentado para categoria (@Input() categoria?: string + withComponentInputBinding(), sin tocar ActivatedRoute), agrega un segundo query param orden a la vista de productos: un link en inicio.html que además de filtrar por categoría, indique un orden (precio), y que Productos lo reciba y lo muestre.
Ver solución
html
<!-- inicio.html -->
<a [routerLink]="['/productos']"
[queryParams]="{categoria: 'ropa', orden: 'precio'}">Ver Ropa por precio</a>
<!-- → http://localhost:4200/productos?categoria=ropa&orden=precio -->ts
// productos.ts
import { Component, Input } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-productos',
imports: [RouterLink],
templateUrl: './productos.html',
styleUrl: './productos.css',
})
export class Productos {
@Input() categoria?: string;
@Input() orden?: string;
}html
<!-- productos.html -->
<p>Categoria filtrada : {{ categoria }}</p>
<p>Orden : {{ orden }}</p>withComponentInputBinding()(ya activado enapp.config.ts) bindea cualquier query param al@Input()del mismo nombre — por eso alcanza con declarar@Input() orden?: stringpara que llegue?orden=precio, sin tocar el guard niActivatedRoute.- El objeto de
[queryParams]puede tener varias claves; Angular arma la URL con&entre ellas (?categoria=ropa&orden=precio).
❓ Preguntas y respuestas
1. ¿Diferencia entre un parámetro de ruta (:id) y un query param (?categoria=...)?
El parámetro de ruta es parte de la URL fija (
path: 'producto/:id'; sin ese segmento la ruta ni matchea); el query param es opcional y no forma parte delpath— sirve para filtros, orden, paginación, etc., y la misma ruta funciona con o sin él.
2. ¿Por qué la ruta ** siempre va al final del array de rutas?
Angular Router evalúa las rutas en el orden del array y usa la primera coincidencia;
**matchea cualquier cosa, así que si estuviera antes "taparía" a todas las rutas siguientes, que nunca se alcanzarían a evaluar.
3. ¿Diferencia entre CanActivate y CanMatch?
Ambos deciden si se puede entrar a una ruta, pero
CanActivateactúa después de que el router ya decidió que esa ruta es la que matchea (si bloquea, la navegación simplemente falla);CanMatchactúa durante la búsqueda de la ruta — si retornafalse, el router puede seguir probando otras rutas con el mismopath, y evita descargar el chunk lazy de esa ruta si no hay permiso.
4. ¿CanDeactivate es suficiente para avisar siempre antes de perder cambios sin guardar?
No — solo cubre navegación dentro del Router de Angular (
routerLink,Router.navigate()). Para cubrir recargar la página, cerrar la pestaña o escribir otra URL directo en la barra de direcciones, hace falta además un listener dewindow:beforeunload(con mensaje genérico, no personalizable). Son mecanismos complementarios.
5. ¿loadComponent vs loadChildren?
loadComponentcarga un solo componente de forma perezosa para una ruta puntual;loadChildrencarga un conjunto de rutas (normalmente una sección completa, comodashboardconperfil/ajustes) de una vez, agrupando varias rutas hijas en el mismo chunk.
6. ¿Por qué inject() en vez de un constructor en un guard funcional?
Un
CanActivateFn(oCanDeactivateFn,CanMatchFn...) es una función, no una clase — no tieneconstructor.inject()permite pedir dependencias del contexto de inyección de Angular dentro de una función, siempre que se llame en el momento correcto (durante la ejecución del guard, dentro de un contexto de inyección válido).
❓ Pendiente
- [ ] Confirmar valor exacto de
[queryParams]tras el cambio "Ver Laptos" (¿laptops?) y actualizar el proyecto de práctica. - [ ] Ver el código de
producto.ts/productos.tsreales del profe para el lado de lectura del query param (confirmar si usa@Input()+withComponentInputBinding()como en el proyecto de práctica, oActivatedRoute.queryParamMap). - [ ] Ver si el profe muestra
loadChildrenparadashboard(agruparperfil/ajustesen un chunk), o si se queda solo conloadComponentpor ruta. - [ ] Confirmar si el profe corrige el link
/perfil(absoluto) que detectamos como error, o si queda así en su proyecto. - [x] Ver el
CanActivateFn(Route Guard) que use el servicioAuthpara proteger una ruta — hecho:auth-guard.tsconectado adashboardconcanActivate: [authGuard]. - [ ]
logueadoquedó fijo entruepara probar el guard sin login real — falta un botón/flujo que llame aauth.cerrarSesion()para probar también el camino bloqueado. - [ ] La ruta
dashboardquedó sinchildren(perfil/ajustes comentados enapp.routes.tspara simplificar mientras se probaba el guard) — falta decidir si se restauran combinadas concanActivateChild, o se dejan comentadas. - [x] Corregir el tipo del guard
puede-saliraCanDeactivateFn<ComponenteConCambiosSinGuardar>y conectarlo — hecho: nueva rutaeditar-perfil(componenteEditarPerfil, queimplements ComponenteConCambiosSinGuardar) concanDeactivate: [puedeSalirGuard]. - [x]
EditarPerfil.formularioTocado— hecho:<input (input)="formularioTocado = true">eneditar-perfil.html. Probado con navegador automatizado: escribir en Nombre + salir por unrouterLinkdispara elconfirm()personalizado; cancelar bloquea la navegación. Funciona. - [x] Cubrir salida por URL manual/recarga/cierre de pestaña — hecho:
@HostListenerdebeforeunloadenEditarPerfil(con el mensaje genérico del navegador, no personalizable — ver sección de arriba).
📎 Apuntes relacionados
- routing.md — Clase 8: bases de routing (
app.routes.ts,provideRouter,routerLink,:id+withComponentInputBinding(), rutas hijas conapp-semana04). Esta nota (Clase 9) es la continuación con temas nuevos. 02-Conceptos.md— pendiente resumir aquí Lazy Loading y query params una vez que se confirme el resto del código real del profe.
➡️ Siguiente
Esperando más capturas/código de la clase para seguir completando Lazy Loading y el resto de pendientes.