Angular PrimerAngular 22 · 2022 → 2026

Routing

`provideRouter` instead of `RouterModule.forRoot`, route titles as data, params bound straight into signal inputs, and guards that are just functions.

The router is a provider now

2022 — module config, and a class guard

@NgModule({ imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })], }) export class AppModule {} @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor(private auth: AuthService, private router: Router) {} canActivate(): boolean { if (!this.auth.isLoggedIn()) { this.router.navigate(['/login']); return false; } return true; } }

2026 — providers, and a function

export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes, withComponentInputBinding(), withViewTransitions(), withInMemoryScrolling()), ], }; export const authGuard: CanActivateFn = () => { const auth = inject(AuthService); const router = inject(Router); return auth.isLoggedIn() ? true : router.createUrlTree(['/login']); };

The functional guard has no class to register, so nothing has to appear in a provider array — and because it returns a UrlTree instead of navigating imperatively, the redirect is expressed as a value the router can act on.

Route titles

The title used to be set in a component's ngOnInit with Title.setTitle, or left to whatever the page happened to be. It is a route property now, and it can be a function of the route:

2026 — titles on the route
export const routes: Routes = [
  { path: '', component: Home, title: 'Angular Primer' },
  { path: 'topics/:slug', loadComponent: () => import('./pages/topic/topic').then((m) => m.Topic),
    title: (route) => `${route.paramMap.get('slug')} — Angular Primer` },
  { path: '**', component: NotFound, title: 'Not found' },
];

Two things follow from putting the title here. The router sets it on navigation without any component doing work, and a prerendered or server-rendered page ships the right <title> in the HTML — which was the part that never worked when the title was set in a lifecycle hook.

Params as signal inputs

withComponentInputBinding() binds route data — params, queryParams, data, resolve — into component inputs, so a page never has to inject ActivatedRoute and subscribe:

2022 — read the param stream, remember to unsubscribe

export class TopicComponent implements OnInit { slug = ''; constructor(private route: ActivatedRoute) {} ngOnInit() { this.route.paramMap.subscribe((params) => { this.slug = params.get('slug')!; this.load(this.slug); }); } }

2026 — an input, bound by the router

export class Topic { readonly slug = input.required<string>(); // route: /topics/:slug private readonly content = resource({ params: () => this.slug(), loader: ({ params }) => fetchPage(params), }); }

The router writes into the input on every navigation, so "the parameter changed" is just "the signal changed" — and anything derived from it, like the resource above, reloads on its own.

Route params as inputs

The routed component takes the parameter as a required input, so it cannot exist without one. Press a link: the component is created with the value already in place.

the address bar would read /orders/…

No route parameter, so the detail component is not created at all — `input.required()` means it cannot exist without one.

With withComponentInputBinding() the router hands :id to the routed component as an input, so the component is created with the value already in place — no ActivatedRoute, no subscription, nothing to unsubscribe. A page cannot mount a second router inside itself, so this demo drives that same input from a signal; the route configuration and the withComponentInputBinding() registration are in the code panel.

Lazy routes without modules

loadComponent for a single component, loadChildren for a slice of routes:

2026 — two flavours of lazy
const routes: Routes = [
  { path: 'cheatsheet', loadComponent: () => import('./pages/cheatsheet/cheatsheet').then((m) => m.Cheatsheet) },
  { path: 'admin', loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
    canActivate: [authGuard] },
];

A guard on a lazy route still runs before the chunk is fetched, which is the behaviour you want: an unauthorised user should not download the admin bundle to be told no.

View transitions

withViewTransitions() wires the browser's View Transitions API into navigation, so a route change can animate between the two pages. Two practical notes: it degrades to an instant swap where the API is missing, and it should respect prefers-reduced-motion — this site disables the transition for users who ask for that.

2026 — enabling it
provideRouter(routes, withViewTransitions({ skipInitialTransition: true }))

A guard is just a function

canMatch reads the session through inject() and answers per navigation. Try it signed out, sign in, try again — the log is the real guard running.

session: signed out · the guard says: none

Try it signed out, then sign in and try again: the guard runs on every navigation, not once at startup, and it reads the same SessionState the component does. The demo calls the real guard function inside the component's injector — the router would call it for you.

Resolvers, still functions

A resolver that blocks navigation until data exists has the same shape as a guard:

2026 — a functional resolver
export const pageResolver: ResolveFn<Page> = (route) => {
  const api = inject(ContentApi);
  return api.page(route.paramMap.get('slug')!);
};

Keep the bar high for using one. A resolver that awaits a slow network call leaves the user staring at the previous page; with signals, rendering the shell and letting a resource() show a skeleton is usually the better experience. Resolvers are for data the page cannot meaningfully render without.

What to take away

Routing became configuration instead of mechanism: providers for setup, functions for the seams where you need logic, and route data flowing into signals so a page reacts to a param the same way it reacts to anything else. Next: HTTP & async data, which is where those signals usually get their values from.