Inicio / Angular / Angular Completo: De Cero a Experto / Routing en Angular

Routing en Angular

Configura rutas, navega entre vistas, usa parámetros de URL, rutas anidadas, lazy loading, guards y resolvers.


Routing en Angular

El Router de Angular gestiona la navegación entre vistas en aplicaciones Single Page Application (SPA). Permite que diferentes URLs muestren diferentes componentes, sin recargar la página.


Configuración inicial

Definir las rutas

// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { SobreNosotrosComponent } from './pages/sobre-nosotros/sobre-nosotros.component';
import { ProductosComponent } from './pages/productos/productos.component';
import { ProductoDetalleComponent } from './pages/producto-detalle/producto-detalle.component';
import { NotFoundComponent } from './pages/not-found/not-found.component';

export const routes: Routes = [
  // Ruta raíz
  { path: '', component: HomeComponent },

  // Rutas simples
  { path: 'sobre-nosotros', component: SobreNosotrosComponent },
  { path: 'productos', component: ProductosComponent },

  // Ruta con parámetro
  { path: 'productos/:id', component: ProductoDetalleComponent },

  // Ruta con múltiples parámetros
  { path: 'cursos/:categoriaId/lecciones/:leccionId', component: LeccionComponent },

  // Redirección
  { path: 'inicio', redirectTo: '', pathMatch: 'full' },

  // Ruta comodín — debe ir SIEMPRE al final
  { path: '**', component: NotFoundComponent }
];

Registrar el router en la app

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withRouterConfig } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withRouterConfig({ onSameUrlNavigation: 'reload' })
    )
  ]
};

Agregar el outlet en el template

// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <nav>
      <a routerLink="/" routerLinkActive="activo" [routerLinkActiveOptions]="{exact:true}">
        Inicio
      </a>
      <a routerLink="/productos" routerLinkActive="activo">Productos</a>
      <a routerLink="/sobre-nosotros" routerLinkActive="activo">Nosotros</a>
    </nav>

    <!-- Aquí se renderizan los componentes de cada ruta -->
    <router-outlet></router-outlet>
  `
})
export class AppComponent {}

Parámetros de ruta

Leer parámetros con ActivatedRoute

import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductosService } from '../services/productos.service';

@Component({
  selector: 'app-producto-detalle',
  standalone: true,
  template: `
    @if (producto) {
      <h1>{{ producto.nombre }}</h1>
      <p>{{ producto.descripcion }}</p>
    } @else if (cargando) {
      <p>Cargando...</p>
    } @else {
      <p>Producto no encontrado</p>
    }
  `
})
export class ProductoDetalleComponent implements OnInit {
  private route = inject(ActivatedRoute);
  private productosService = inject(ProductosService);

  producto: any = null;
  cargando = true;

  ngOnInit(): void {
    // Forma 1: snapshot (solo el valor inicial, no reactivo)
    const id = Number(this.route.snapshot.paramMap.get('id'));
    this.cargarProducto(id);

    // Forma 2: Observable (reactivo — detecta cambios en la URL)
    this.route.paramMap.subscribe(params => {
      const id = Number(params.get('id'));
      this.cargarProducto(id);
    });
  }

  private cargarProducto(id: number): void {
    this.cargando = true;
    this.producto = this.productosService.obtenerPorId(id);
    this.cargando = false;
  }
}

Query Parameters

// URL: /productos?categoria=electronica&orden=precio
ngOnInit(): void {
  // Snapshot
  const categoria = this.route.snapshot.queryParamMap.get('categoria');

  // Observable
  this.route.queryParamMap.subscribe(params => {
    const categoria = params.get('categoria');
    const orden = params.get('orden');
    const pagina = Number(params.get('pagina')) || 1;
  });
}

Navegación programática

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-login',
  standalone: true,
  template: `
    <button (click)="irAlInicio()">Ir al inicio</button>
    <button (click)="irAProducto(42)">Ver producto 42</button>
    <button (click)="irConQueryParams()">Buscar</button>
  `
})
export class LoginComponent {
  private router = inject(Router);

  irAlInicio(): void {
    this.router.navigate(['/']);
  }

  irAProducto(id: number): void {
    this.router.navigate(['/productos', id]);
    // equivale a: /productos/42
  }

  irConQueryParams(): void {
    this.router.navigate(['/productos'], {
      queryParams: { categoria: 'electronica', pagina: 1 }
    });
    // equivale a: /productos?categoria=electronica&pagina=1
  }

  // Navegación relativa
  irARelativo(): void {
    this.router.navigate(['../otra-ruta'], { relativeTo: this.route });
  }

  // Reemplazar en el historial (sin crear nueva entrada)
  irSinHistorial(): void {
    this.router.navigate(['/home'], { replaceUrl: true });
  }
}

Rutas anidadas (Child Routes)

// app.routes.ts
export const routes: Routes = [
  {
    path: 'admin',
    component: AdminLayoutComponent,
    children: [
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
      { path: 'dashboard', component: AdminDashboardComponent },
      { path: 'usuarios', component: AdminUsuariosComponent },
      { path: 'usuarios/:id/editar', component: EditarUsuarioComponent },
      { path: 'productos', component: AdminProductosComponent }
    ]
  }
];
// admin-layout.component.ts — debe tener su propio <router-outlet>
@Component({
  selector: 'app-admin-layout',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <aside>
      <nav>
        <a routerLink="dashboard" routerLinkActive="activo">Dashboard</a>
        <a routerLink="usuarios" routerLinkActive="activo">Usuarios</a>
        <a routerLink="productos" routerLinkActive="activo">Productos</a>
      </nav>
    </aside>
    <main>
      <router-outlet></router-outlet>  <!-- Rutas hijas aquí -->
    </main>
  `
})
export class AdminLayoutComponent {}

Lazy Loading — Carga perezosa

El lazy loading carga el código de una ruta solo cuando se accede a ella, reduciendo el bundle inicial:

// app.routes.ts
export const routes: Routes = [
  { path: '', component: HomeComponent },

  // Lazy loading de un componente standalone
  {
    path: 'admin',
    loadComponent: () =>
      import('./pages/admin/admin.component').then(m => m.AdminComponent)
  },

  // Lazy loading de un grupo de rutas (más común)
  {
    path: 'tienda',
    loadChildren: () =>
      import('./pages/tienda/tienda.routes').then(m => m.tiendaRoutes)
  }
];
// tienda/tienda.routes.ts — rutas del módulo tienda
import { Routes } from '@angular/router';

export const tiendaRoutes: Routes = [
  { path: '', component: TiendaHomeComponent },
  { path: 'catalogo', component: CatalogoComponent },
  { path: 'carrito', component: CarritoComponent },
  { path: 'checkout', component: CheckoutComponent }
];

Guards — Rutas protegidas

Los Guards deciden si se puede acceder a una ruta:

canActivate — Proteger acceso

// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.estaLogueado()) {
    return true;
  }

  // Guardar la URL a la que intentaba acceder
  router.navigate(['/login'], {
    queryParams: { returnUrl: state.url }
  });
  return false;
};

// Guard para verificar rol
export const adminGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.esAdmin()) {
    return true;
  }

  router.navigate(['/sin-permiso']);
  return false;
};
// Aplicar los guards en las rutas
export const routes: Routes = [
  { path: 'login', component: LoginComponent },
  {
    path: 'perfil',
    component: PerfilComponent,
    canActivate: [authGuard]   // ← Solo accesible si está logueado
  },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [authGuard, adminGuard]  // ← Debe estar logueado Y ser admin
  }
];

canDeactivate — Prevenir salida

// unsaved-changes.guard.ts
import { CanDeactivateFn } from '@angular/router';

export interface PuedeSalir {
  tieneCambiosSinGuardar(): boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<PuedeSalir> = (componente) => {
  if (componente.tieneCambiosSinGuardar()) {
    return confirm('Tienes cambios sin guardar. ¿Deseas salir?');
  }
  return true;
};
// Componente de formulario
@Component({ selector: 'app-editar-perfil', ... })
export class EditarPerfilComponent implements PuedeSalir {
  formularioModificado = false;

  tieneCambiosSinGuardar(): boolean {
    return this.formularioModificado;
  }
}

// En las rutas
{ path: 'editar-perfil', component: EditarPerfilComponent, canDeactivate: [unsavedChangesGuard] }

Resolvers — Precargar datos antes de entrar a una ruta

// producto.resolver.ts
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { ProductosService } from '../services/productos.service';

export const productoResolver: ResolveFn<Producto> = (route) => {
  const id = Number(route.paramMap.get('id'));
  return inject(ProductosService).obtenerPorId(id);
  // También puede retornar un Observable o Promise
};
// En las rutas
{ path: 'productos/:id', component: ProductoDetalleComponent, resolve: { producto: productoResolver } }

// En el componente — los datos ya están disponibles
ngOnInit(): void {
  this.producto = this.route.snapshot.data['producto'];
}

RouterLink avanzado

<!-- Link simple -->
<a routerLink="/productos">Productos</a>

<!-- Link con parámetros -->
<a [routerLink]="['/productos', producto.id]">Ver producto</a>

<!-- Link con query params -->
<a [routerLink]="['/buscar']" [queryParams]="{q: 'angular', pagina: 1}">
  Buscar Angular
</a>

<!-- Link con fragment (#seccion) -->
<a [routerLink]="['/docs']" fragment="instalacion">
  Ir a instalación
</a>

<!-- Preservar query params existentes -->
<a [routerLink]="['/otra-pagina']" queryParamsHandling="preserve">
  Otra página
</a>

<!-- Clase activa personalizada -->
<a routerLink="/inicio" routerLinkActive="mi-clase-activa">Inicio</a>

<!-- Solo activo en ruta exacta (no en sub-rutas) -->
<a routerLink="/" routerLinkActive="activo"
   [routerLinkActiveOptions]="{exact: true}">
  Inicio
</a>

Configuración del router

// app.config.ts
import { provideRouter, withHashLocation, withViewTransitions, withPreloading, PreloadAllModules } from '@angular/router';

export const appConfig = {
  providers: [
    provideRouter(
      routes,
      withHashLocation(),           // URLs con # (para hosting sin configuración)
      withViewTransitions(),         // Transiciones animadas entre vistas (Chrome)
      withPreloading(PreloadAllModules)  // Precarga todos los módulos lazy en segundo plano
    )
  ]
};

Resumen

Concepto Función
Routes Array de configuración de rutas
RouterOutlet Donde se renderizan los componentes
RouterLink Navegación declarativa en templates
RouterLinkActive Clase CSS para link activo
ActivatedRoute Leer parámetros de la ruta actual
Router Navegación programática
canActivate Guard para proteger acceso
canDeactivate Guard para prevenir salida
resolve Precargar datos antes de la ruta
loadComponent / loadChildren Lazy loading