@real-router/angular 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/README.md ADDED
@@ -0,0 +1,454 @@
1
+ # @real-router/angular
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](../../LICENSE)
4
+
5
+ > Angular 21 integration for [Real-Router](https://github.com/greydragon888/real-router) — inject functions, components, and directives.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @real-router/angular @real-router/core @real-router/browser-plugin
11
+ ```
12
+
13
+ **Peer dependencies:** `@angular/core` >= 21.0.0, `@angular/common` >= 21.0.0
14
+
15
+ ## Quick Start
16
+
17
+ Bootstrap a standalone Angular application with `provideRealRouter`:
18
+
19
+ ```typescript
20
+ import { bootstrapApplication } from "@angular/platform-browser";
21
+ import { createRouter } from "@real-router/core";
22
+ import { browserPluginFactory } from "@real-router/browser-plugin";
23
+ import { provideRealRouter } from "@real-router/angular";
24
+ import { AppComponent } from "./app.component";
25
+
26
+ const router = createRouter([
27
+ { name: "home", path: "/" },
28
+ {
29
+ name: "users",
30
+ path: "/users",
31
+ children: [{ name: "profile", path: "/:id" }],
32
+ },
33
+ ]);
34
+
35
+ router.usePlugin(browserPluginFactory());
36
+ await router.start();
37
+
38
+ bootstrapApplication(AppComponent, {
39
+ providers: [provideRealRouter(router)],
40
+ });
41
+ ```
42
+
43
+ Then use `injectRoute` and `RouteView` in your root component:
44
+
45
+ ```typescript
46
+ import { Component } from "@angular/core";
47
+ import {
48
+ injectRoute,
49
+ RouteView,
50
+ RouteMatch,
51
+ RouteNotFound,
52
+ RealLink,
53
+ } from "@real-router/angular";
54
+
55
+ @Component({
56
+ selector: "app-root",
57
+ imports: [RouteView, RouteMatch, RouteNotFound, RealLink],
58
+ template: `
59
+ <nav>
60
+ <a realLink routeName="home">Home</a>
61
+ <a realLink routeName="users">Users</a>
62
+ </nav>
63
+
64
+ <route-view [routeNode]="''">
65
+ <ng-template routeMatch="home">
66
+ <app-home />
67
+ </ng-template>
68
+ <ng-template routeMatch="users">
69
+ <app-users-layout />
70
+ </ng-template>
71
+ <ng-template routeNotFound>
72
+ <app-not-found />
73
+ </ng-template>
74
+ </route-view>
75
+ `,
76
+ })
77
+ export class AppComponent {
78
+ readonly route = injectRoute();
79
+ }
80
+ ```
81
+
82
+ For nested children (e.g., `users.profile`), place another `<route-view>` inside the parent layout and set `routeNode` to the parent's name:
83
+
84
+ ```typescript
85
+ @Component({
86
+ selector: "app-users-layout",
87
+ imports: [RouteView, RouteMatch],
88
+ template: `
89
+ <h1>Users</h1>
90
+ <route-view [routeNode]="'users'">
91
+ <ng-template routeMatch="profile">
92
+ <app-user-profile />
93
+ </ng-template>
94
+ </route-view>
95
+ `,
96
+ })
97
+ export class UsersLayoutComponent {}
98
+ ```
99
+
100
+ ## Functions
101
+
102
+ 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.
103
+
104
+ | Function | Returns | Reactive? |
105
+ | ------------------------------------------- | ---------------------------------- | ------------------------------------ |
106
+ | `injectRouter()` | `Router` | Never |
107
+ | `injectNavigator()` | `Navigator` | Never |
108
+ | `injectRoute()` | `RouteSignals` | `routeState` on every navigation |
109
+ | `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) |
110
+ | `injectRouteUtils()` | `RouteUtils` | Never |
111
+ | `injectRouterTransition()` | `Signal<RouterTransitionSnapshot>` | On transition start/end |
112
+ | `injectIsActiveRoute(name, params?, opts?)` | `Signal<boolean>` | On active state change |
113
+
114
+ `RouteSignals` shape:
115
+
116
+ ```typescript
117
+ interface RouteSignals {
118
+ readonly routeState: Signal<RouteSnapshot>; // { route, previousRoute }
119
+ readonly navigator: Navigator;
120
+ }
121
+ ```
122
+
123
+ ```typescript
124
+ // injectRouteNode — updates only when "users.*" changes
125
+ @Component({
126
+ selector: "app-users-layout",
127
+ template: `
128
+ @if (route.routeState().route; as r) {
129
+ @switch (r.name) {
130
+ @case ("users") {
131
+ <app-users-list />
132
+ }
133
+ @case ("users.profile") {
134
+ <app-user-profile [id]="r.params['id']" />
135
+ }
136
+ }
137
+ }
138
+ `,
139
+ })
140
+ export class UsersLayoutComponent {
141
+ readonly route = injectRouteNode("users");
142
+ }
143
+
144
+ // injectNavigator — stable reference, never reactive
145
+ @Component({
146
+ selector: "app-back-button",
147
+ template: `<button (click)="goHome()">Back</button>`,
148
+ })
149
+ export class BackButtonComponent {
150
+ private readonly navigator = injectNavigator();
151
+
152
+ goHome(): void {
153
+ this.navigator.navigate("home");
154
+ }
155
+ }
156
+
157
+ // injectRouterTransition — progress bars, loading states
158
+ @Component({
159
+ selector: "app-progress",
160
+ template: `
161
+ @if (transition().isTransitioning) {
162
+ <div class="progress-bar"></div>
163
+ }
164
+ `,
165
+ })
166
+ export class ProgressComponent {
167
+ readonly transition = injectRouterTransition();
168
+ }
169
+ ```
170
+
171
+ ## Components
172
+
173
+ ### `<route-view>`
174
+
175
+ Declarative route matching. Renders the first `ng-template[routeMatch]` whose segment matches the active route.
176
+
177
+ ```html
178
+ <route-view [routeNode]="''">
179
+ <ng-template routeMatch="users">
180
+ <app-users />
181
+ </ng-template>
182
+ <ng-template routeMatch="settings">
183
+ <app-settings />
184
+ </ng-template>
185
+ <ng-template routeNotFound>
186
+ <app-not-found />
187
+ </ng-template>
188
+ </route-view>
189
+ ```
190
+
191
+ 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:
192
+
193
+ ```html
194
+ <!-- Nested layout: renders when any "users.*" route is active -->
195
+ <route-view [routeNode]="'users'">
196
+ <ng-template routeMatch="profile">
197
+ <app-user-profile />
198
+ </ng-template>
199
+ </route-view>
200
+ ```
201
+
202
+ > **Note:** The input is named `routeNode` (not `nodeName`) because `nodeName` is a read-only property on `HTMLElement`. Angular's template binding would fail with the unaliased name.
203
+
204
+ ### `<router-error-boundary>`
205
+
206
+ 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.
207
+
208
+ ```typescript
209
+ import { RouterErrorBoundary, type ErrorContext } from "@real-router/angular";
210
+ ```
211
+
212
+ ```html
213
+ <router-error-boundary
214
+ [errorTemplate]="errorTpl"
215
+ (onError)="onNavError($event)"
216
+ >
217
+ <a realLink routeName="protected">Go to Protected</a>
218
+ </router-error-boundary>
219
+
220
+ <ng-template #errorTpl let-error let-reset="resetError">
221
+ <div class="toast">
222
+ {{ error.code }}
223
+ <button (click)="reset()">Dismiss</button>
224
+ </div>
225
+ </ng-template>
226
+ ```
227
+
228
+ The template context is typed as `ErrorContext`:
229
+
230
+ ```typescript
231
+ interface ErrorContext {
232
+ $implicit: RouterError; // the navigation error
233
+ resetError: () => void; // dismiss the error
234
+ }
235
+ ```
236
+
237
+ Auto-resets on the next successful navigation. Works with both `realLink` and imperative `router.navigate()`.
238
+
239
+ ### `<navigation-announcer>`
240
+
241
+ WCAG-compliant screen reader announcements for route changes. Add it once near the root of your application:
242
+
243
+ ```html
244
+ <navigation-announcer />
245
+ ```
246
+
247
+ See the [Accessibility](#accessibility) section for details.
248
+
249
+ ## Directives
250
+
251
+ ### `realLink`
252
+
253
+ Navigation directive for `<a>` elements. Handles click events, sets `href`, and applies an active CSS class automatically.
254
+
255
+ ```html
256
+ <a realLink routeName="users.profile" [routeParams]="{ id: '123' }">
257
+ View Profile
258
+ </a>
259
+
260
+ <a
261
+ realLink
262
+ routeName="users.profile"
263
+ [routeParams]="{ id: '123' }"
264
+ activeClassName="is-active"
265
+ [activeStrict]="false"
266
+ [ignoreQueryParams]="true"
267
+ [routeOptions]="{ replace: true }"
268
+ >
269
+ View Profile
270
+ </a>
271
+ ```
272
+
273
+ | Input | Type | Default | Description |
274
+ | ------------------- | ------------------- | ---------- | -------------------------------------- |
275
+ | `routeName` | `string` | `""` | Target route name |
276
+ | `routeParams` | `Params` | `{}` | Route parameters |
277
+ | `routeOptions` | `NavigationOptions` | `{}` | Navigation options (replace, etc.) |
278
+ | `activeClassName` | `string` | `"active"` | CSS class applied when route is active |
279
+ | `activeStrict` | `boolean` | `false` | Exact match only (no ancestor match) |
280
+ | `ignoreQueryParams` | `boolean` | `true` | Query params don't affect active state |
281
+
282
+ ### `[realLinkActive]`
283
+
284
+ 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.
285
+
286
+ ```html
287
+ <li [realLinkActive]="'active'" routeName="users" [routeParams]="{}">
288
+ <a realLink routeName="users">Users</a>
289
+ </li>
290
+ ```
291
+
292
+ | Input | Type | Default | Description |
293
+ | ------------------- | --------- | ------- | -------------------------------------- |
294
+ | `realLinkActive` | `string` | `""` | CSS class to apply when active |
295
+ | `routeName` | `string` | `""` | Route to watch |
296
+ | `routeParams` | `Params` | `{}` | Route parameters |
297
+ | `activeStrict` | `boolean` | `false` | Exact match only |
298
+ | `ignoreQueryParams` | `boolean` | `true` | Query params don't affect active state |
299
+
300
+ ### `routeMatch`
301
+
302
+ Structural directive used inside `<route-view>`. Marks an `ng-template` as the content to render when a route segment matches.
303
+
304
+ ```html
305
+ <ng-template routeMatch="home">
306
+ <app-home />
307
+ </ng-template>
308
+ ```
309
+
310
+ ### `routeNotFound`
311
+
312
+ Structural directive used inside `<route-view>`. Marks an `ng-template` as the fallback when no segment matches and the route is `UNKNOWN_ROUTE`.
313
+
314
+ ```html
315
+ <ng-template routeNotFound>
316
+ <app-not-found />
317
+ </ng-template>
318
+ ```
319
+
320
+ ## Accessibility
321
+
322
+ Add `<navigation-announcer>` once near the root of your application to enable WCAG-compliant screen reader announcements on every route change:
323
+
324
+ ```typescript
325
+ import { NavigationAnnouncer } from "@real-router/angular";
326
+
327
+ @Component({
328
+ selector: "app-root",
329
+ imports: [NavigationAnnouncer],
330
+ template: `
331
+ <navigation-announcer />
332
+ <!-- rest of your app -->
333
+ `,
334
+ })
335
+ export class AppComponent {}
336
+ ```
337
+
338
+ The announcer creates a visually hidden `aria-live` region and announces each navigation to screen readers. See the [Accessibility guide](https://github.com/greydragon888/real-router/wiki/Accessibility) for details.
339
+
340
+ ## Angular-Specific Patterns
341
+
342
+ ### Signals, Not Observables
343
+
344
+ `injectRoute()` and `injectRouteNode()` return Angular signals, not RxJS observables. Read them in templates directly or call them in computed/effect:
345
+
346
+ ```typescript
347
+ @Component({
348
+ template: `
349
+ @if (route.routeState().route; as r) {
350
+ <h1>{{ r.name }}</h1>
351
+ }
352
+ `,
353
+ })
354
+ export class PageComponent {
355
+ readonly route = injectRoute();
356
+ }
357
+ ```
358
+
359
+ To react to changes in class code, use `effect`:
360
+
361
+ ```typescript
362
+ import { effect } from "@angular/core";
363
+
364
+ export class PageComponent {
365
+ readonly route = injectRouteNode("users");
366
+
367
+ constructor() {
368
+ effect(() => {
369
+ const r = this.route.routeState().route;
370
+ if (r) {
371
+ document.title = `Users — ${r.params["id"] ?? "list"}`;
372
+ }
373
+ });
374
+ }
375
+ }
376
+ ```
377
+
378
+ ### Injection Context
379
+
380
+ All `inject*` functions must be called within an injection context. The constructor and field initializers are both valid:
381
+
382
+ ```typescript
383
+ // Field initializer — preferred
384
+ export class MyComponent {
385
+ readonly route = injectRoute(); // valid
386
+ }
387
+
388
+ // Constructor — also valid
389
+ export class MyComponent {
390
+ readonly route: RouteSignals;
391
+
392
+ constructor() {
393
+ this.route = injectRoute(); // valid
394
+ }
395
+ }
396
+
397
+ // Outside injection context — throws
398
+ export class MyComponent {
399
+ ngOnInit() {
400
+ const route = injectRoute(); // ERROR: not in injection context
401
+ }
402
+ }
403
+ ```
404
+
405
+ `sourceToSignal` follows the same rule — it calls `inject(DestroyRef)` internally.
406
+
407
+ ### DestroyRef for Cleanup
408
+
409
+ Subscriptions created by `sourceToSignal` and the directives clean up automatically via `DestroyRef.onDestroy`. No manual unsubscribe needed.
410
+
411
+ ### Zoneless Compatibility
412
+
413
+ The adapter is signal-first and does not depend on Zone.js. It works with `provideExperimentalZonelessChangeDetection()` out of the box.
414
+
415
+ ### ngOnInit for Input-Dependent Setup
416
+
417
+ `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`.
418
+
419
+ ## Signal Bridge
420
+
421
+ ### `sourceToSignal(source)`
422
+
423
+ 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.
424
+
425
+ ```typescript
426
+ import { sourceToSignal } from "@real-router/angular";
427
+ import { createTransitionSource } from "@real-router/sources";
428
+
429
+ const transitionSignal = sourceToSignal(createTransitionSource(router));
430
+ ```
431
+
432
+ ## Documentation
433
+
434
+ Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
435
+
436
+ - [RouterProvider](https://github.com/greydragon888/real-router/wiki/RouterProvider) · [RouteView](https://github.com/greydragon888/real-router/wiki/RouteView) · [RouterErrorBoundary](https://github.com/greydragon888/real-router/wiki/RouterErrorBoundary)
437
+ - [injectRouter](https://github.com/greydragon888/real-router/wiki/injectRouter) · [injectRoute](https://github.com/greydragon888/real-router/wiki/injectRoute) · [injectRouteNode](https://github.com/greydragon888/real-router/wiki/injectRouteNode) · [injectNavigator](https://github.com/greydragon888/real-router/wiki/injectNavigator) · [injectRouteUtils](https://github.com/greydragon888/real-router/wiki/injectRouteUtils) · [injectRouterTransition](https://github.com/greydragon888/real-router/wiki/injectRouterTransition)
438
+
439
+ ## Related Packages
440
+
441
+ | Package | Description |
442
+ | ---------------------------------------------------------------------------------------- | --------------------------------------- |
443
+ | [@real-router/core](https://www.npmjs.com/package/@real-router/core) | Core router (required dependency) |
444
+ | [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) | Browser History API integration |
445
+ | [@real-router/sources](https://www.npmjs.com/package/@real-router/sources) | Subscription layer (used internally) |
446
+ | [@real-router/route-utils](https://www.npmjs.com/package/@real-router/route-utils) | Route tree queries (`injectRouteUtils`) |
447
+
448
+ ## Contributing
449
+
450
+ See [contributing guidelines](../../CONTRIBUTING.md) for development setup and PR process.
451
+
452
+ ## License
453
+
454
+ [MIT](../../LICENSE) © [Oleg Ivanov](https://github.com/greydragon888)