Modern Angular: Signals, Standalone Components and SSR · Routing, State, Performance and SSR
Routing: Lazy Loading, Guards, Resolvers and Input Binding
Configure routes with lazy-loaded components, protect them with functional guards, load data with resolvers, and bind route parameters to component inputs.
What you'll build
A router configuration with a product page whose :id parameter and resolved product arrive as component inputs, a resolver that redirects to a not-found page for unknown IDs, an admin area that's lazy-loaded only after a functional guard allows it, and a login page that receives the return URL as an input. RouterTestingHarness navigates and prints each result.
The idea
provideRouter(routes, withComponentInputBinding())binds path params, query params and resolved data toinput()s with matching names.loadComponent: () => import("./admin.page")...splits the admin code into a separate chunk loaded on first visit.- Functional guards (
CanActivateFn) returntrueor aUrlTreeto redirect; they caninject()services. - Resolvers (
ResolveFn) load data before activation; return aRedirectCommandto send users elsewhere. - Guards are UX: the server API must still enforce permissions.
The code
// file: admin.page.ts
import { Component } from "@angular/core";
console.log(" (admin chunk loaded)");
@Component({ template: `<h1>Admin</h1><p>Orders today: 42</p>` })
export class AdminPage {}
// file: main.ts
import { Component, Injectable, inject, input, signal } from "@angular/core";
import { CanActivateFn, RedirectCommand, ResolveFn, Router, Routes, provideRouter, withComponentInputBinding } from "@angular/router";
import { RouterTestingHarness } from "@angular/router/testing";
import { TestBed } from "@angular/core/testing";
import { BrowserTestingModule, platformBrowserTesting } from "@angular/platform-browser/testing";
type Product = { id: string; name: string };
const catalog: Record<string, Product> = { "1": { id: "1", name: "Mug" }, "2": { id: "2", name: "Teapot" } };
@Injectable({ providedIn: "root" })
class AuthService {
readonly role = signal<"guest" | "admin">("guest");
}
const productResolver: ResolveFn<Product> = (route) =>
catalog[route.paramMap.get("id")!] ?? new RedirectCommand(inject(Router).parseUrl("/not-found"));
const adminGuard: CanActivateFn = (_route, state) =>
inject(AuthService).role() === "admin" ||
inject(Router).createUrlTree(["/login"], { queryParams: { returnUrl: state.url } });
@Component({ template: `<h1>{{ product().name }}</h1><p>Product id {{ id() }}</p>` })
class ProductPage {
id = input.required<string>(); // from the :id path parameter
product = input.required<Product>(); // from the resolver
}
@Component({ template: `<h1>Log in</h1><p>Then back to {{ returnUrl() }}</p>` })
class LoginPage {
returnUrl = input("/"); // from the ?returnUrl query parameter
}
@Component({ template: `<h1>Page not found</h1>` })
class NotFoundPage {}
const routes: Routes = [
{ path: "products/:id", component: ProductPage, resolve: { product: productResolver } },
{ path: "admin", canActivate: [adminGuard], loadComponent: () => import("./admin.page").then((m) => m.AdminPage) },
{ path: "login", component: LoginPage },
{ path: "**", component: NotFoundPage },
];
TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
TestBed.configureTestingModule({ providers: [provideRouter(routes, withComponentInputBinding())] });
const harness = await RouterTestingHarness.create();
const router = TestBed.inject(Router);
async function visit(url: string) {
await harness.navigateByUrl(url);
const text = [...harness.routeNativeElement!.querySelectorAll("h1, p")].map((n) => n.textContent!.trim()).join(" | ");
console.log(`${url} -> ${router.url}: ${text}`);
}
await visit("/products/2");
await visit("/products/9");
await visit("/admin");
TestBed.inject(AuthService).role.set("admin");
await visit("/admin");Output
/products/2 -> /products/2: Teapot | Product id 2 /products/9 -> /not-found: Page not found /admin -> /login?returnUrl=%2Fadmin: Log in | Then back to /admin (admin chunk loaded) /admin -> /admin: Admin | Orders today: 42
Run this on your computer — see “Get Started” for setup.
The admin chunk loads only on the visit the guard allows; the first /admin visit redirects to login with the return URL as an input.
Common mistakes
- Reading params with
ActivatedRoutesubscriptions everywhere instead ofwithComponentInputBinding()and inputs. - Eagerly importing every page in the routes file, so the first load ships the whole app; lazy-load feature areas.
- Treating guards as security; the API must check permissions too.
- Resolvers that block navigation on slow requests with no feedback; resolve only what the page can't render without, and load the rest in the component.
Key points
- withComponentInputBinding passes params, query params and resolved data to inputs.
- loadComponent and loadChildren split code per route.
- Functional guards and resolvers can inject services and redirect.
Further reading: Routing overview · Route guards · Data resolvers · Lazy loading
Practice questions
What does withComponentInputBinding do?
Answer
It binds route path parameters, query parameters and resolved data to component inputs with matching names, so components don't need ActivatedRoute.
How does a functional guard redirect?
Answer
It returns a UrlTree (for example router.createUrlTree(["/login"])) or a RedirectCommand instead of true, and the router navigates there.
Why lazy-load routes?
Answer
The route's code is split into a separate chunk loaded on first navigation, keeping the initial bundle small.
Open in the app to mark lessons complete, take notes and track your progress.