@real-router/angular
v0.8.1
Published
Angular 21 integration for Real-Router
Maintainers
Readme
@real-router/angular
Angular 21 integration for Real-Router — inject functions, components, and directives.
Installation
npm install @real-router/angular @real-router/core @real-router/browser-pluginPeer dependencies: @angular/core >= 21.0.0, @angular/common >= 21.0.0
Quick Start
Bootstrap a standalone Angular application with provideRealRouter:
import { bootstrapApplication } from "@angular/platform-browser";
import { createRouter } from "@real-router/core";
import { browserPluginFactory } from "@real-router/browser-plugin";
import { provideRealRouter } from "@real-router/angular";
import { AppComponent } from "./app.component";
const router = createRouter([
{ name: "home", path: "/" },
{
name: "users",
path: "/users",
children: [{ name: "profile", path: "/:id" }],
},
]);
router.usePlugin(browserPluginFactory());
await router.start();
bootstrapApplication(AppComponent, {
providers: [provideRealRouter(router)],
});Then use injectRoute and RouteView in your root component:
import { Component } from "@angular/core";
import {
injectRoute,
RouteView,
RouteMatch,
RouteNotFound,
RealLink,
} from "@real-router/angular";
@Component({
selector: "app-root",
imports: [RouteView, RouteMatch, RouteNotFound, RealLink],
template: `
<nav>
<a realLink routeName="home">Home</a>
<a realLink routeName="users">Users</a>
</nav>
<route-view [routeNode]="''">
<ng-template routeMatch="home">
<app-home />
</ng-template>
<ng-template routeMatch="users">
<app-users-layout />
</ng-template>
<ng-template routeNotFound>
<app-not-found />
</ng-template>
</route-view>
`,
})
export class AppComponent {
readonly route = injectRoute();
}For nested children (e.g., users.profile), place another <route-view> inside the parent layout and set routeNode to the parent's name:
@Component({
selector: "app-users-layout",
imports: [RouteView, RouteMatch],
template: `
<h1>Users</h1>
<route-view [routeNode]="'users'">
<ng-template routeMatch="profile">
<app-user-profile />
</ng-template>
</route-view>
`,
})
export class UsersLayoutComponent {}Functions
All inject functions must be called within an injection context (constructor, field initializer, or runInInjectionContext). Route state functions return RouteSignals — an object with a routeState signal and a stable navigator reference.
| Function | Returns | Reactive? |
| ------------------------------------------- | ---------------------------------- | ------------------------------------ |
| injectRouter() | Router | Never |
| injectNavigator() | Navigator | Never |
| injectRoute() | RouteSignals | routeState on every navigation |
| injectRouteNode(name) | RouteSignals | When the node subtree is entered, left, or changes between descendants (uses shouldUpdateNode — sibling-leaf transitions within the same subtree still fire) |
| injectRouteUtils() | RouteUtils | Never |
| injectRouterTransition() | Signal<RouterTransitionSnapshot> | On transition start/end |
| injectIsActiveRoute(name, params?, opts?) | Signal<boolean> | On active state change |
| injectRouteExit(handler, options?) | void — wraps subscribeLeave with abort + same-route guards | Never (handler captured at injection time) |
| injectRouteEnter(handler, options?) | void — fires once on nav-driven mount via effect() + transition.from | Never (handler captured at injection time) |
RouteSignals shape:
interface RouteSignals {
readonly routeState: Signal<RouteSnapshot>; // { route, previousRoute }
readonly navigator: Navigator;
}// injectRouteNode — updates only when "users.*" changes
@Component({
selector: "app-users-layout",
template: `
@if (route.routeState().route; as r) {
@switch (r.name) {
@case ("users") {
<app-users-list />
}
@case ("users.profile") {
<app-user-profile [id]="r.params['id']" />
}
}
}
`,
})
export class UsersLayoutComponent {
readonly route = injectRouteNode("users");
}
// injectNavigator — stable reference, never reactive
@Component({
selector: "app-back-button",
template: `<button (click)="goHome()">Back</button>`,
})
export class BackButtonComponent {
private readonly navigator = injectNavigator();
goHome(): void {
this.navigator.navigate("home");
}
}
// injectRouterTransition — progress bars, loading states
@Component({
selector: "app-progress",
template: `
@if (transition().isTransitioning) {
<div class="progress-bar"></div>
}
`,
})
export class ProgressComponent {
readonly transition = injectRouterTransition();
}
// injectRouteExit — exit animations, draft autosave, AbortSignal-aware cleanup
@Component({
selector: "app-fade-out",
template: `<div #box>...</div>`,
})
export class FadeOutComponent {
private el = viewChild.required<ElementRef<HTMLDivElement>>("box");
constructor() {
injectRouteExit(async ({ signal }) => {
const el = this.el().nativeElement;
el.classList.add("fade-out");
const cleanup = () => el.classList.remove("fade-out");
signal.addEventListener("abort", cleanup, { once: true });
el.getBoundingClientRect(); // style flush
await Promise.allSettled(el.getAnimations().map((a) => a.finished));
cleanup();
});
}
}
// injectRouteEnter — page-enter analytics, focus management, entry animations
@Component({ selector: "app-page-enter", template: "" })
export class PageEnterAnalyticsComponent {
constructor() {
injectRouteEnter(({ route, previousRoute }) => {
analytics.track("page_enter", {
route: route.name,
from: previousRoute.name,
});
});
}
}Angular handler-reactivity:
inject*functions run once at construction, sohandleris captured at injection time. Pass a class method (stable identity) and read signals inside the handler body to react to changes. See CLAUDE.md → "injectRouteExit / injectRouteEnter Handler Is Captured At Injection Time".
Components
<route-view>
Declarative route matching. Renders the first ng-template[routeMatch] whose segment matches the active route.
<route-view [routeNode]="''">
<ng-template routeMatch="users">
<app-users />
</ng-template>
<ng-template routeMatch="settings">
<app-settings />
</ng-template>
<ng-template routeNotFound>
<app-not-found />
</ng-template>
</route-view>The routeNode input (aliased from nodeName) scopes the view to a subtree. Pass "" for the root level, or a route name like "users" for nested layouts:
<!-- Nested layout: renders when any "users.*" route is active -->
<route-view [routeNode]="'users'">
<ng-template routeMatch="profile">
<app-user-profile />
</ng-template>
</route-view>Note: The input is named
routeNode(notnodeName) becausenodeNameis a read-only property onHTMLElement. Angular's template binding would fail with the unaliased name.
<router-error-boundary>
Declarative error handling for navigation errors. Renders its content normally and shows an error template alongside it when a guard rejects or a route is not found.
import { RouterErrorBoundary, type ErrorContext } from "@real-router/angular";<router-error-boundary
[errorTemplate]="errorTpl"
(onError)="onNavError($event)"
>
<a realLink routeName="protected">Go to Protected</a>
</router-error-boundary>
<ng-template #errorTpl let-error let-reset="resetError">
<div class="toast">
{{ error.code }}
<button (click)="reset()">Dismiss</button>
</div>
</ng-template>The template context is typed as ErrorContext:
interface ErrorContext {
$implicit: RouterError; // the navigation error
resetError: () => void; // dismiss the error
}Auto-resets on the next successful navigation. Works with both realLink and imperative router.navigate().
<navigation-announcer>
WCAG-compliant screen reader announcements for route changes. Add it once near the root of your application:
<navigation-announcer />See the Accessibility section for details.
Directives
realLink
Navigation directive for <a> elements. Handles click events, sets href, and applies an active CSS class automatically.
<a realLink routeName="users.profile" [routeParams]="{ id: '123' }">
View Profile
</a>
<a
realLink
routeName="users.profile"
[routeParams]="{ id: '123' }"
activeClassName="is-active"
[activeStrict]="false"
[ignoreQueryParams]="true"
[routeOptions]="{ replace: true }"
>
View Profile
</a>| Input | Type | Default | Description |
| ------------------- | ------------------- | ---------- | -------------------------------------- |
| routeName | string | "" | Target route name |
| routeParams | Params | {} | Route parameters |
| routeOptions | NavigationOptions | {} | Navigation options (replace, etc.) |
| activeClassName | string | "active" | CSS class applied when route is active |
| activeStrict | boolean | false | Exact match only (no ancestor match) |
| ignoreQueryParams | boolean | true | Query params don't affect active state |
| hash | string | undefined| URL fragment (decoded). Tri-state: undefined preserves, "" clears, value sets. (#532) |
hash input — URL fragment / tab-style UIs
<a [realLink]="'settings'" [hash]="'profile'">Profile</a>
<a [realLink]="'settings'" [hash]="'account'">Account</a>Active class is hash-aware — only the matching tab lights up. Live demo: examples/web/react/link-hash/ — behavior is identical across adapters, only template syntax differs. See the Hash Fragment Support wiki page for the full surface.
[realLinkActive]
Applies an active CSS class to any element when a route is active. Use this when you need active state on a non-<a> element, or when the clickable element and the styled element are different.
<li [realLinkActive]="'active'" routeName="users" [routeParams]="{}">
<a realLink routeName="users">Users</a>
</li>| Input | Type | Default | Description |
| ------------------- | --------- | ------- | -------------------------------------- |
| realLinkActive | string | "" | CSS class to apply when active |
| routeName | string | "" | Route to watch |
| routeParams | Params | {} | Route parameters |
| activeStrict | boolean | false | Exact match only |
| ignoreQueryParams | boolean | true | Query params don't affect active state |
routeMatch
Structural directive used inside <route-view>. Marks an ng-template as the content to render when a route segment matches.
<ng-template routeMatch="home">
<app-home />
</ng-template>routeNotFound
Structural directive used inside <route-view>. Marks an ng-template as the fallback when no segment matches and the route is UNKNOWN_ROUTE.
<ng-template routeNotFound>
<app-not-found />
</ng-template>Accessibility
Add <navigation-announcer> once near the root of your application to enable WCAG-compliant screen reader announcements on every route change:
import { NavigationAnnouncer } from "@real-router/angular";
@Component({
selector: "app-root",
imports: [NavigationAnnouncer],
template: `
<navigation-announcer />
<!-- rest of your app -->
`,
})
export class AppComponent {}The announcer creates a visually hidden aria-live region and announces each navigation to screen readers. See the Accessibility guide for details.
Scroll Restoration
Opt-in via the provideRealRouter options bag:
import { provideRealRouter } from "@real-router/angular";
bootstrapApplication(AppComponent, {
providers: [
provideRealRouter(router, {
scrollRestoration: { mode: "restore" },
}),
],
});RealRouterOptions shape:
interface RealRouterOptions {
scrollRestoration?: ScrollRestorationOptions; // { mode?, anchorScrolling?, scrollContainer? }
viewTransitions?: boolean;
}Restores scroll on back/forward, scrolls to top (or #hash) on push. Three modes: "restore" (default), "top", "native". Custom containers via scrollContainer: () => HTMLElement | null. The utility is created by provideEnvironmentInitializer and torn down via inject(DestroyRef). Options are a snapshot at bootstrap — not reactive to runtime changes. See Scroll Restoration guide for details.
View Transitions
Opt-in animated route transitions via the browser's View Transitions API:
import { provideRealRouter } from "@real-router/angular";
bootstrapApplication(AppComponent, {
providers: [
provideRealRouter(router, { viewTransitions: true }),
],
});No-op on unsupported browsers (Firefox as of 2026-04, SSR). Utility is created by provideEnvironmentInitializer at bootstrap and torn down via inject(DestroyRef). Option is a snapshot at bootstrap — not reactive to runtime changes. Customization is pure CSS via ::view-transition-* pseudo-elements and view-transition-name for hero morphs. See View Transitions guide for patterns.
Angular-Specific Patterns
Signals, Not Observables
injectRoute() and injectRouteNode() return Angular signals, not RxJS observables. Read them in templates directly or call them in computed/effect:
@Component({
template: `
@if (route.routeState().route; as r) {
<h1>{{ r.name }}</h1>
}
`,
})
export class PageComponent {
readonly route = injectRoute();
}To react to changes in class code, use effect:
import { effect } from "@angular/core";
export class PageComponent {
readonly route = injectRouteNode("users");
constructor() {
effect(() => {
const r = this.route.routeState().route;
if (r) {
document.title = `Users — ${r.params["id"] ?? "list"}`;
}
});
}
}Injection Context
All inject* functions must be called within an injection context. The constructor and field initializers are both valid:
// Field initializer — preferred
export class MyComponent {
readonly route = injectRoute(); // valid
}
// Constructor — also valid
export class MyComponent {
readonly route: RouteSignals;
constructor() {
this.route = injectRoute(); // valid
}
}
// Outside injection context — throws
export class MyComponent {
ngOnInit() {
const route = injectRoute(); // ERROR: not in injection context
}
}sourceToSignal follows the same rule — it calls inject(DestroyRef) internally.
DestroyRef for Cleanup
Subscriptions created by sourceToSignal and the directives clean up automatically via DestroyRef.onDestroy. No manual unsubscribe needed.
Zoneless Compatibility
The adapter is signal-first and does not depend on Zone.js. It works with provideExperimentalZonelessChangeDetection() out of the box.
ngOnInit for Input-Dependent Setup
RealLink, RealLinkActive, and RouteView create their subscription sources in ngOnInit, not the constructor. Signal inputs (input()) are not available during construction, so setup that reads inputs must be deferred to ngOnInit.
Signal Bridge
sourceToSignal(source)
Bridges any RouterSource<T> (from @real-router/sources) into an Angular Signal<T>. Cleanup wires through inject(DestroyRef) — must be called in an injection context. Used internally by RouterErrorBoundary; exposed for custom composables that need to bridge router sources into reactive signals.
import { sourceToSignal } from "@real-router/angular";
import { createTransitionSource } from "@real-router/sources";
const transitionSignal = sourceToSignal(createTransitionSource(router));Documentation
Full documentation: Wiki
- RouterProvider · RouteView · RouterErrorBoundary · Scroll Restoration
- injectRouter · injectRoute · injectRouteNode · injectNavigator · injectRouteUtils · injectRouterTransition · injectRouteExit · injectRouteEnter
Related Packages
| Package | Description |
| ---------------------------------------------------------------------------------------- | --------------------------------------- |
| @real-router/core | Core router (required dependency) |
| @real-router/browser-plugin | Browser History API integration |
| @real-router/sources | Subscription layer (used internally) |
| @real-router/route-utils | Route tree queries (injectRouteUtils) |
Contributing
See contributing guidelines for development setup and PR process.
