@real-router/angular 0.8.0 → 0.9.0

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.
Files changed (43) hide show
  1. package/README.md +183 -4
  2. package/dist/README.md +183 -4
  3. package/dist/fesm2022/real-router-angular-ssr.mjs +323 -0
  4. package/dist/fesm2022/real-router-angular-ssr.mjs.map +1 -0
  5. package/dist/fesm2022/real-router-angular.mjs +759 -173
  6. package/dist/fesm2022/real-router-angular.mjs.map +1 -1
  7. package/dist/types/real-router-angular-ssr.d.ts +227 -0
  8. package/dist/types/real-router-angular-ssr.d.ts.map +1 -0
  9. package/dist/types/real-router-angular.d.ts +119 -20
  10. package/dist/types/real-router-angular.d.ts.map +1 -1
  11. package/package.json +18 -11
  12. package/src/components/RouteView.ts +81 -56
  13. package/src/components/RouterErrorBoundary.ts +7 -5
  14. package/src/directives/RealLink.ts +57 -37
  15. package/src/directives/RealLinkActive.ts +34 -25
  16. package/src/dom-utils/link-utils.ts +119 -7
  17. package/src/dom-utils/route-announcer.ts +58 -2
  18. package/src/dom-utils/scroll-restore.ts +160 -12
  19. package/src/functions/injectIsActiveRoute.ts +9 -8
  20. package/src/functions/injectNavigator.ts +4 -0
  21. package/src/functions/injectOrThrow.ts +5 -1
  22. package/src/functions/injectRoute.ts +17 -8
  23. package/src/functions/injectRouteEnter.ts +5 -10
  24. package/src/functions/injectRouteNode.ts +3 -0
  25. package/src/functions/injectRouteUtils.ts +3 -0
  26. package/src/functions/injectRouter.ts +4 -0
  27. package/src/functions/injectRouterTransition.ts +3 -0
  28. package/src/index.ts +14 -3
  29. package/src/internal/buildActiveRouteOptions.ts +20 -0
  30. package/src/internal/install.ts +77 -0
  31. package/src/internal/subscribeSourceToSignal.ts +48 -0
  32. package/src/providers.ts +11 -38
  33. package/src/providersFactory.ts +298 -0
  34. package/src/sourceToSignal.ts +10 -2
  35. package/src/types.ts +6 -1
  36. package/ssr/components/ClientOnly.ts +27 -0
  37. package/ssr/components/HttpStatusCode.ts +106 -0
  38. package/ssr/components/ServerOnly.ts +27 -0
  39. package/ssr/functions/injectDeferred.ts +92 -0
  40. package/ssr/functions/provideHttpStatusSink.ts +43 -0
  41. package/ssr/ng-package.json +6 -0
  42. package/ssr/public_api.ts +35 -0
  43. package/ssr/utils/createHttpStatusSink.ts +61 -0
package/README.md CHANGED
@@ -40,6 +40,8 @@ bootstrapApplication(AppComponent, {
40
40
  });
41
41
  ```
42
42
 
43
+ > **Lifecycle:** `provideRealRouter(router)` expects a router that has already been started — `await router.start()` MUST run before `bootstrapApplication`. For SSR / SSG, use [`provideRealRouterFactory`](#server-side-rendering) instead — it accepts a non-started `baseRouter` and runs `router.start(url)` itself via `provideAppInitializer`, deriving the URL from Angular's `REQUEST` token.
44
+
43
45
  Then use `injectRoute` and `RouteView` in your root component:
44
46
 
45
47
  ```typescript
@@ -288,6 +290,103 @@ WCAG-compliant screen reader announcements for route changes. Add it once near t
288
290
 
289
291
  See the [Accessibility](#accessibility) section for details.
290
292
 
293
+ ### `<client-only>` / `<server-only>` (`@real-router/angular/ssr`)
294
+
295
+ Paired SSR-aware boundaries. `<client-only>` renders the bound `fallback` `TemplateRef` on the server (and on the client first paint, to match SSR HTML), then swaps in the projected children after mount. `<server-only>` is the symmetric inverse.
296
+
297
+ Imported from the `/ssr` subpath (ng-packagr secondary entry-point). The same `/ssr` entry also exposes `injectDeferred()` — see [packages/angular/CLAUDE.md](./CLAUDE.md) — for cross-adapter parity with `@real-router/{react,preact,solid,vue,svelte}/ssr`.
298
+
299
+ ```typescript
300
+ import { Component } from "@angular/core";
301
+ import { ClientOnly, ServerOnly } from "@real-router/angular/ssr";
302
+
303
+ @Component({
304
+ selector: "app-home",
305
+ template: `
306
+ <ng-template #loadingTpl>
307
+ <span>Loading…</span>
308
+ </ng-template>
309
+ <client-only [fallback]="loadingTpl">
310
+ <browser-api-widget />
311
+ </client-only>
312
+
313
+ <server-only>
314
+ <seo-meta-strip />
315
+ </server-only>
316
+ `,
317
+ imports: [ClientOnly, ServerOnly],
318
+ })
319
+ export class HomeComponent {}
320
+ ```
321
+
322
+ Implementation: `signal(false)` + `afterNextRender(() => mounted.set(true))`. `afterNextRender` is a no-op on the server (Angular runtime guarantees), so SSR naturally lands on the SSR-side branch. End-to-end dogfooding lives in [`examples/web/angular/ssr-examples/ssr/`](../../examples/web/angular/ssr-examples/ssr/) (see `e2e/ssr-boundaries.spec.ts`).
323
+
324
+ ### `<http-status-code>` (`@real-router/angular/ssr`)
325
+
326
+ Render-time HTTP status declaration. Writes `code` to the optional `HttpStatusSink` provided via `provideHttpStatusSink`, then renders nothing. Last write wins. No-op when no sink is provided.
327
+
328
+ ```typescript
329
+ // not-found.component.ts
330
+ import { Component } from "@angular/core";
331
+ import { HttpStatusCode } from "@real-router/angular/ssr";
332
+
333
+ @Component({
334
+ selector: "app-not-found",
335
+ imports: [HttpStatusCode],
336
+ template: `
337
+ <http-status-code [code]="404" />
338
+ <h1>Page not found</h1>
339
+ `,
340
+ })
341
+ export class NotFoundComponent {}
342
+ ```
343
+
344
+ ```typescript
345
+ // entry-server.ts
346
+ import { bootstrapApplication } from "@angular/platform-browser";
347
+ import {
348
+ createHttpStatusSink,
349
+ provideHttpStatusSink,
350
+ } from "@real-router/angular/ssr";
351
+
352
+ const sink = createHttpStatusSink();
353
+
354
+ await bootstrapApplication(AppRoot, {
355
+ providers: [
356
+ provideRealRouterFactory({ baseRouter }),
357
+ provideHttpStatusSink(sink),
358
+ ],
359
+ });
360
+
361
+ response.status(sink.code ?? 200).send(html);
362
+ ```
363
+
364
+ `HTTP_STATUS_SINK` is the underlying `InjectionToken` — inject it directly with `{ optional: true }` if you need to read the sink in your own components. `createHttpStatusSink()` constructs a fresh `{ code: number | undefined }` per request — read `sink.code` after the SSR render pass to set the response status. Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep working as before; `<http-status-code>` covers render-time decisions only.
365
+
366
+ ### `injectDeferred()` (`@real-router/angular/ssr`)
367
+
368
+ Reads `state.context.ssrDataDeferred[key]` (populated by `defer()` in `@real-router/ssr-data-plugin`). Returns `Signal<T | undefined>` — `undefined` before the promise settles, the resolved value once it does. Compose with `@if` or the `async` pipe for pending UI:
369
+
370
+ ```typescript
371
+ import { Component } from "@angular/core";
372
+ import { injectDeferred } from "@real-router/angular/ssr";
373
+
374
+ @Component({
375
+ template: `
376
+ @if (reviews(); as r) {
377
+ @for (review of r; track review.id) { <li>{{ review.author }}</li> }
378
+ } @else {
379
+ <p>Loading reviews…</p>
380
+ }
381
+ `,
382
+ })
383
+ export class ReviewsComponent {
384
+ readonly reviews = injectDeferred<Review[]>("reviews");
385
+ }
386
+ ```
387
+
388
+ **Full `/ssr` surface** (8 exports): `ClientOnly`, `ServerOnly`, `HttpStatusCode`, `injectDeferred`, `provideHttpStatusSink`, `HTTP_STATUS_SINK`, `createHttpStatusSink`, plus the `HttpStatusSink` type. See [`packages/angular/CLAUDE.md`](./CLAUDE.md#ssr-feature-surface--real-routerangularssr) for the implementation notes.
389
+
291
390
  ## Directives
292
391
 
293
392
  ### `realLink`
@@ -359,6 +458,25 @@ Structural directive used inside `<route-view>`. Marks an `ng-template` as the c
359
458
  </ng-template>
360
459
  ```
361
460
 
461
+ ### `routeSelf`
462
+
463
+ Structural directive used inside `<route-view>`. Marks an `ng-template` as the exact-match slot for the parent `<route-view>`'s `routeNode` — it renders only when `state.name === routeNode()`. Useful for nodes that have both an "index" view and child routes:
464
+
465
+ ```html
466
+ <route-view [routeNode]="'users'">
467
+ <ng-template routeSelf>
468
+ <!-- shown when route is exactly "users" -->
469
+ <app-users-list />
470
+ </ng-template>
471
+ <ng-template routeMatch="profile">
472
+ <!-- shown when route is "users.profile" -->
473
+ <app-user-profile />
474
+ </ng-template>
475
+ </route-view>
476
+ ```
477
+
478
+ **Template priority** inside `<route-view>`: `routeMatch` (segment prefix) → `routeSelf` (exact-match for `routeNode`) → `routeNotFound` (`UNKNOWN_ROUTE` only). First-wins for `routeMatch` and `routeSelf`, last-wins for `routeNotFound`.
479
+
362
480
  ### `routeNotFound`
363
481
 
364
482
  Structural directive used inside `<route-view>`. Marks an `ng-template` as the fallback when no segment matches and the route is `UNKNOWN_ROUTE`.
@@ -416,6 +534,65 @@ interface RealRouterOptions {
416
534
 
417
535
  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](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) for details.
418
536
 
537
+ ## Server-Side Rendering
538
+
539
+ For Angular SSR (`@angular/ssr` with `outputMode: "server"`) and SSG build-time render via `renderApplication`, use `provideRealRouterFactory` instead of `provideRealRouter`. The factory creates a per-request router clone via Angular's `REQUEST: InjectionToken<Request | null>`, runs `router.start(url)` through `provideAppInitializer`, and disposes the router on `DestroyRef`:
540
+
541
+ ```typescript
542
+ import { provideRealRouterFactory } from "@real-router/angular";
543
+ import { browserPluginFactory } from "@real-router/browser-plugin";
544
+ import { ssrDataPluginFactory } from "@real-router/ssr-data-plugin";
545
+
546
+ const baseRouter = createRouter(routes);
547
+
548
+ export const appConfig: ApplicationConfig = {
549
+ providers: [
550
+ provideRealRouterFactory({
551
+ baseRouter,
552
+ plugins: (request) =>
553
+ request
554
+ ? [ssrDataPluginFactory(loaders)]
555
+ : [browserPluginFactory(), ssrDataPluginFactory(loaders)],
556
+ deps: (request) => ({
557
+ currentUser: request
558
+ ? parseCookies(request.headers.get("cookie"))
559
+ : parseCookies(document.cookie),
560
+ }),
561
+ }),
562
+ ],
563
+ };
564
+ ```
565
+
566
+ Existing `provideRealRouter(router)` is unchanged — keep using it for SPA / post-hydrate scenarios. Both APIs ship in parallel; pick one for the whole application.
567
+
568
+ ### Working examples
569
+
570
+ **SPA examples** — `provideRealRouter(router)` after `await router.start()`:
571
+
572
+ | Example | Demonstrates |
573
+ |---------|--------------|
574
+ | [`examples/web/angular/basic/`](../../examples/web/angular/basic) | Minimal setup with `RouteView` + `RealLink` + `injectRoute` |
575
+ | [`examples/web/angular/combined/`](../../examples/web/angular/combined) | All features combined: nested routes, dynamic params, lazy loading, persistent params |
576
+ | [`examples/web/angular/dynamic-routes/`](../../examples/web/angular/dynamic-routes) | `:id` params, programmatic navigation |
577
+ | [`examples/web/angular/hash-routing/`](../../examples/web/angular/hash-routing) | `hash-plugin` with `<a realLink hash="…">` tab-style UIs (#532) |
578
+ | [`examples/web/angular/lazy-loading/`](../../examples/web/angular/lazy-loading) | Route-level code-splitting via `import()` |
579
+ | [`examples/web/angular/nested-routes/`](../../examples/web/angular/nested-routes) | Multi-level `<route-view>` composition |
580
+ | [`examples/web/angular/persistent-params/`](../../examples/web/angular/persistent-params) | `persistent-params-plugin` integration |
581
+ | [`examples/web/angular/animation-examples/`](../../examples/web/angular/animation-examples) | View Transitions API + scroll restoration + direction-tracker patterns |
582
+
583
+ **SSR / SSG examples** — `provideRealRouterFactory({ baseRouter, plugins, deps })`:
584
+
585
+ | Example | Demonstrates |
586
+ |---------|--------------|
587
+ | [`examples/web/angular/ssr-examples/ssr/`](../../examples/web/angular/ssr-examples/ssr) | Classical SSR with cookie-based DI, auth guards, nested loaders |
588
+ | [`examples/web/angular/ssr-examples/ssr-mixed/`](../../examples/web/angular/ssr-examples/ssr-mixed) | Mixed SSR/CSR routes — some routes server-rendered, others CSR-only |
589
+ | [`examples/web/angular/ssr-examples/ssr-streaming/`](../../examples/web/angular/ssr-examples/ssr-streaming) | Streaming SSR with `@defer (on viewport)` + `@defer (on hover)` + `withIncrementalHydration()` |
590
+ | [`examples/web/angular/ssr-examples/ssg/`](../../examples/web/angular/ssr-examples/ssg) | Static site generation via in-process AngularNodeAppEngine + `getStaticPaths()` |
591
+
592
+ **Post-hydration loader skip (#599)** — `provideRealRouterFactory` automatically bridges Angular's `TransferState` to the cross-adapter hydration scratchpad. On the server pass, the resolved router state is written to `TransferState`; on the client, the bootstrap consumes the seed via `hydrateRouter(...)` and `ssr-data-plugin` reuses the server-resolved `state.context.data` without re-invoking the loader on first paint. Requires `provideServerRendering()` (server) + `provideClientHydration()` (client) — both standard for Angular SSR apps. Verified end-to-end in `ssr/` and `ssr-streaming/` examples via `window.__LOADER_CALLS__` counter assertion.
593
+
594
+ See [CLAUDE.md → SSR Support](./CLAUDE.md#ssr-support) for the full decision matrix, lifecycle diagram, plugin separation guidance, decision matrix, and known constraints.
595
+
419
596
  ## View Transitions
420
597
 
421
598
  Opt-in animated route transitions via the browser's [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API):
@@ -507,9 +684,9 @@ Subscriptions created by `sourceToSignal` and the directives clean up automatica
507
684
 
508
685
  The adapter is signal-first and does not depend on Zone.js. It works with `provideExperimentalZonelessChangeDetection()` out of the box.
509
686
 
510
- ### ngOnInit for Input-Dependent Setup
687
+ ### Reactive Source Setup via `effect()` (#630)
511
688
 
512
- `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`.
689
+ `RealLink`, `RealLinkActive`, and `RouteView` create their subscription sources inside `effect(...)` blocks scheduled from the **constructor** (not `ngOnInit`). Reading signal inputs inside `effect()` makes the source-creation REACTIVE — when `[realLink]`, `[routeParams]`, `[hash]`, `[realLinkActive]`, or `[routeNode]` change in AOT, the effect tears down the previous source via `onCleanup` and creates a new one with the current input values. The legacy `ngOnInit` setup captured inputs once at mount and produced a real AOT bug (#630). Effect cleanup is bound automatically to the host directive's injection-context `DestroyRef`.
513
690
 
514
691
  ## Signal Bridge
515
692
 
@@ -526,10 +703,12 @@ const transitionSignal = sourceToSignal(createTransitionSource(router));
526
703
 
527
704
  ## Documentation
528
705
 
529
- Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
706
+ Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki) — start with the [Angular Integration guide](https://github.com/greydragon888/real-router/wiki/Angular-Integration) for Angular-specific examples and gotchas.
707
+
708
+ The shared (cross-framework) wiki pages use the `use*` naming convention — they cover every adapter (React, Preact, Solid, Vue, Svelte, Angular) and each page has an explicit Angular section showing the `inject*` form:
530
709
 
531
710
  - [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) · [Scroll Restoration](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration)
532
- - [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) · [injectRouteExit](https://github.com/greydragon888/real-router/wiki/injectRouteExit) · [injectRouteEnter](https://github.com/greydragon888/real-router/wiki/injectRouteEnter)
711
+ - [useRouter → `injectRouter`](https://github.com/greydragon888/real-router/wiki/useRouter) · [useRoute → `injectRoute`](https://github.com/greydragon888/real-router/wiki/useRoute) · [useRouteNode → `injectRouteNode`](https://github.com/greydragon888/real-router/wiki/useRouteNode) · [useNavigator → `injectNavigator`](https://github.com/greydragon888/real-router/wiki/useNavigator) · [useRouteUtils → `injectRouteUtils`](https://github.com/greydragon888/real-router/wiki/useRouteUtils) · [useRouterTransition → `injectRouterTransition`](https://github.com/greydragon888/real-router/wiki/useRouterTransition) · [useRouteExit → `injectRouteExit`](https://github.com/greydragon888/real-router/wiki/useRouteExit) · [useRouteEnter → `injectRouteEnter`](https://github.com/greydragon888/real-router/wiki/useRouteEnter)
533
712
 
534
713
  ## Related Packages
535
714
 
package/dist/README.md CHANGED
@@ -40,6 +40,8 @@ bootstrapApplication(AppComponent, {
40
40
  });
41
41
  ```
42
42
 
43
+ > **Lifecycle:** `provideRealRouter(router)` expects a router that has already been started — `await router.start()` MUST run before `bootstrapApplication`. For SSR / SSG, use [`provideRealRouterFactory`](#server-side-rendering) instead — it accepts a non-started `baseRouter` and runs `router.start(url)` itself via `provideAppInitializer`, deriving the URL from Angular's `REQUEST` token.
44
+
43
45
  Then use `injectRoute` and `RouteView` in your root component:
44
46
 
45
47
  ```typescript
@@ -288,6 +290,103 @@ WCAG-compliant screen reader announcements for route changes. Add it once near t
288
290
 
289
291
  See the [Accessibility](#accessibility) section for details.
290
292
 
293
+ ### `<client-only>` / `<server-only>` (`@real-router/angular/ssr`)
294
+
295
+ Paired SSR-aware boundaries. `<client-only>` renders the bound `fallback` `TemplateRef` on the server (and on the client first paint, to match SSR HTML), then swaps in the projected children after mount. `<server-only>` is the symmetric inverse.
296
+
297
+ Imported from the `/ssr` subpath (ng-packagr secondary entry-point). The same `/ssr` entry also exposes `injectDeferred()` — see [packages/angular/CLAUDE.md](./CLAUDE.md) — for cross-adapter parity with `@real-router/{react,preact,solid,vue,svelte}/ssr`.
298
+
299
+ ```typescript
300
+ import { Component } from "@angular/core";
301
+ import { ClientOnly, ServerOnly } from "@real-router/angular/ssr";
302
+
303
+ @Component({
304
+ selector: "app-home",
305
+ template: `
306
+ <ng-template #loadingTpl>
307
+ <span>Loading…</span>
308
+ </ng-template>
309
+ <client-only [fallback]="loadingTpl">
310
+ <browser-api-widget />
311
+ </client-only>
312
+
313
+ <server-only>
314
+ <seo-meta-strip />
315
+ </server-only>
316
+ `,
317
+ imports: [ClientOnly, ServerOnly],
318
+ })
319
+ export class HomeComponent {}
320
+ ```
321
+
322
+ Implementation: `signal(false)` + `afterNextRender(() => mounted.set(true))`. `afterNextRender` is a no-op on the server (Angular runtime guarantees), so SSR naturally lands on the SSR-side branch. End-to-end dogfooding lives in [`examples/web/angular/ssr-examples/ssr/`](../../examples/web/angular/ssr-examples/ssr/) (see `e2e/ssr-boundaries.spec.ts`).
323
+
324
+ ### `<http-status-code>` (`@real-router/angular/ssr`)
325
+
326
+ Render-time HTTP status declaration. Writes `code` to the optional `HttpStatusSink` provided via `provideHttpStatusSink`, then renders nothing. Last write wins. No-op when no sink is provided.
327
+
328
+ ```typescript
329
+ // not-found.component.ts
330
+ import { Component } from "@angular/core";
331
+ import { HttpStatusCode } from "@real-router/angular/ssr";
332
+
333
+ @Component({
334
+ selector: "app-not-found",
335
+ imports: [HttpStatusCode],
336
+ template: `
337
+ <http-status-code [code]="404" />
338
+ <h1>Page not found</h1>
339
+ `,
340
+ })
341
+ export class NotFoundComponent {}
342
+ ```
343
+
344
+ ```typescript
345
+ // entry-server.ts
346
+ import { bootstrapApplication } from "@angular/platform-browser";
347
+ import {
348
+ createHttpStatusSink,
349
+ provideHttpStatusSink,
350
+ } from "@real-router/angular/ssr";
351
+
352
+ const sink = createHttpStatusSink();
353
+
354
+ await bootstrapApplication(AppRoot, {
355
+ providers: [
356
+ provideRealRouterFactory({ baseRouter }),
357
+ provideHttpStatusSink(sink),
358
+ ],
359
+ });
360
+
361
+ response.status(sink.code ?? 200).send(html);
362
+ ```
363
+
364
+ `HTTP_STATUS_SINK` is the underlying `InjectionToken` — inject it directly with `{ optional: true }` if you need to read the sink in your own components. `createHttpStatusSink()` constructs a fresh `{ code: number | undefined }` per request — read `sink.code` after the SSR render pass to set the response status. Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep working as before; `<http-status-code>` covers render-time decisions only.
365
+
366
+ ### `injectDeferred()` (`@real-router/angular/ssr`)
367
+
368
+ Reads `state.context.ssrDataDeferred[key]` (populated by `defer()` in `@real-router/ssr-data-plugin`). Returns `Signal<T | undefined>` — `undefined` before the promise settles, the resolved value once it does. Compose with `@if` or the `async` pipe for pending UI:
369
+
370
+ ```typescript
371
+ import { Component } from "@angular/core";
372
+ import { injectDeferred } from "@real-router/angular/ssr";
373
+
374
+ @Component({
375
+ template: `
376
+ @if (reviews(); as r) {
377
+ @for (review of r; track review.id) { <li>{{ review.author }}</li> }
378
+ } @else {
379
+ <p>Loading reviews…</p>
380
+ }
381
+ `,
382
+ })
383
+ export class ReviewsComponent {
384
+ readonly reviews = injectDeferred<Review[]>("reviews");
385
+ }
386
+ ```
387
+
388
+ **Full `/ssr` surface** (8 exports): `ClientOnly`, `ServerOnly`, `HttpStatusCode`, `injectDeferred`, `provideHttpStatusSink`, `HTTP_STATUS_SINK`, `createHttpStatusSink`, plus the `HttpStatusSink` type. See [`packages/angular/CLAUDE.md`](./CLAUDE.md#ssr-feature-surface--real-routerangularssr) for the implementation notes.
389
+
291
390
  ## Directives
292
391
 
293
392
  ### `realLink`
@@ -359,6 +458,25 @@ Structural directive used inside `<route-view>`. Marks an `ng-template` as the c
359
458
  </ng-template>
360
459
  ```
361
460
 
461
+ ### `routeSelf`
462
+
463
+ Structural directive used inside `<route-view>`. Marks an `ng-template` as the exact-match slot for the parent `<route-view>`'s `routeNode` — it renders only when `state.name === routeNode()`. Useful for nodes that have both an "index" view and child routes:
464
+
465
+ ```html
466
+ <route-view [routeNode]="'users'">
467
+ <ng-template routeSelf>
468
+ <!-- shown when route is exactly "users" -->
469
+ <app-users-list />
470
+ </ng-template>
471
+ <ng-template routeMatch="profile">
472
+ <!-- shown when route is "users.profile" -->
473
+ <app-user-profile />
474
+ </ng-template>
475
+ </route-view>
476
+ ```
477
+
478
+ **Template priority** inside `<route-view>`: `routeMatch` (segment prefix) → `routeSelf` (exact-match for `routeNode`) → `routeNotFound` (`UNKNOWN_ROUTE` only). First-wins for `routeMatch` and `routeSelf`, last-wins for `routeNotFound`.
479
+
362
480
  ### `routeNotFound`
363
481
 
364
482
  Structural directive used inside `<route-view>`. Marks an `ng-template` as the fallback when no segment matches and the route is `UNKNOWN_ROUTE`.
@@ -416,6 +534,65 @@ interface RealRouterOptions {
416
534
 
417
535
  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](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) for details.
418
536
 
537
+ ## Server-Side Rendering
538
+
539
+ For Angular SSR (`@angular/ssr` with `outputMode: "server"`) and SSG build-time render via `renderApplication`, use `provideRealRouterFactory` instead of `provideRealRouter`. The factory creates a per-request router clone via Angular's `REQUEST: InjectionToken<Request | null>`, runs `router.start(url)` through `provideAppInitializer`, and disposes the router on `DestroyRef`:
540
+
541
+ ```typescript
542
+ import { provideRealRouterFactory } from "@real-router/angular";
543
+ import { browserPluginFactory } from "@real-router/browser-plugin";
544
+ import { ssrDataPluginFactory } from "@real-router/ssr-data-plugin";
545
+
546
+ const baseRouter = createRouter(routes);
547
+
548
+ export const appConfig: ApplicationConfig = {
549
+ providers: [
550
+ provideRealRouterFactory({
551
+ baseRouter,
552
+ plugins: (request) =>
553
+ request
554
+ ? [ssrDataPluginFactory(loaders)]
555
+ : [browserPluginFactory(), ssrDataPluginFactory(loaders)],
556
+ deps: (request) => ({
557
+ currentUser: request
558
+ ? parseCookies(request.headers.get("cookie"))
559
+ : parseCookies(document.cookie),
560
+ }),
561
+ }),
562
+ ],
563
+ };
564
+ ```
565
+
566
+ Existing `provideRealRouter(router)` is unchanged — keep using it for SPA / post-hydrate scenarios. Both APIs ship in parallel; pick one for the whole application.
567
+
568
+ ### Working examples
569
+
570
+ **SPA examples** — `provideRealRouter(router)` after `await router.start()`:
571
+
572
+ | Example | Demonstrates |
573
+ |---------|--------------|
574
+ | [`examples/web/angular/basic/`](../../examples/web/angular/basic) | Minimal setup with `RouteView` + `RealLink` + `injectRoute` |
575
+ | [`examples/web/angular/combined/`](../../examples/web/angular/combined) | All features combined: nested routes, dynamic params, lazy loading, persistent params |
576
+ | [`examples/web/angular/dynamic-routes/`](../../examples/web/angular/dynamic-routes) | `:id` params, programmatic navigation |
577
+ | [`examples/web/angular/hash-routing/`](../../examples/web/angular/hash-routing) | `hash-plugin` with `<a realLink hash="…">` tab-style UIs (#532) |
578
+ | [`examples/web/angular/lazy-loading/`](../../examples/web/angular/lazy-loading) | Route-level code-splitting via `import()` |
579
+ | [`examples/web/angular/nested-routes/`](../../examples/web/angular/nested-routes) | Multi-level `<route-view>` composition |
580
+ | [`examples/web/angular/persistent-params/`](../../examples/web/angular/persistent-params) | `persistent-params-plugin` integration |
581
+ | [`examples/web/angular/animation-examples/`](../../examples/web/angular/animation-examples) | View Transitions API + scroll restoration + direction-tracker patterns |
582
+
583
+ **SSR / SSG examples** — `provideRealRouterFactory({ baseRouter, plugins, deps })`:
584
+
585
+ | Example | Demonstrates |
586
+ |---------|--------------|
587
+ | [`examples/web/angular/ssr-examples/ssr/`](../../examples/web/angular/ssr-examples/ssr) | Classical SSR with cookie-based DI, auth guards, nested loaders |
588
+ | [`examples/web/angular/ssr-examples/ssr-mixed/`](../../examples/web/angular/ssr-examples/ssr-mixed) | Mixed SSR/CSR routes — some routes server-rendered, others CSR-only |
589
+ | [`examples/web/angular/ssr-examples/ssr-streaming/`](../../examples/web/angular/ssr-examples/ssr-streaming) | Streaming SSR with `@defer (on viewport)` + `@defer (on hover)` + `withIncrementalHydration()` |
590
+ | [`examples/web/angular/ssr-examples/ssg/`](../../examples/web/angular/ssr-examples/ssg) | Static site generation via in-process AngularNodeAppEngine + `getStaticPaths()` |
591
+
592
+ **Post-hydration loader skip (#599)** — `provideRealRouterFactory` automatically bridges Angular's `TransferState` to the cross-adapter hydration scratchpad. On the server pass, the resolved router state is written to `TransferState`; on the client, the bootstrap consumes the seed via `hydrateRouter(...)` and `ssr-data-plugin` reuses the server-resolved `state.context.data` without re-invoking the loader on first paint. Requires `provideServerRendering()` (server) + `provideClientHydration()` (client) — both standard for Angular SSR apps. Verified end-to-end in `ssr/` and `ssr-streaming/` examples via `window.__LOADER_CALLS__` counter assertion.
593
+
594
+ See [CLAUDE.md → SSR Support](./CLAUDE.md#ssr-support) for the full decision matrix, lifecycle diagram, plugin separation guidance, decision matrix, and known constraints.
595
+
419
596
  ## View Transitions
420
597
 
421
598
  Opt-in animated route transitions via the browser's [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API):
@@ -507,9 +684,9 @@ Subscriptions created by `sourceToSignal` and the directives clean up automatica
507
684
 
508
685
  The adapter is signal-first and does not depend on Zone.js. It works with `provideExperimentalZonelessChangeDetection()` out of the box.
509
686
 
510
- ### ngOnInit for Input-Dependent Setup
687
+ ### Reactive Source Setup via `effect()` (#630)
511
688
 
512
- `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`.
689
+ `RealLink`, `RealLinkActive`, and `RouteView` create their subscription sources inside `effect(...)` blocks scheduled from the **constructor** (not `ngOnInit`). Reading signal inputs inside `effect()` makes the source-creation REACTIVE — when `[realLink]`, `[routeParams]`, `[hash]`, `[realLinkActive]`, or `[routeNode]` change in AOT, the effect tears down the previous source via `onCleanup` and creates a new one with the current input values. The legacy `ngOnInit` setup captured inputs once at mount and produced a real AOT bug (#630). Effect cleanup is bound automatically to the host directive's injection-context `DestroyRef`.
513
690
 
514
691
  ## Signal Bridge
515
692
 
@@ -526,10 +703,12 @@ const transitionSignal = sourceToSignal(createTransitionSource(router));
526
703
 
527
704
  ## Documentation
528
705
 
529
- Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
706
+ Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki) — start with the [Angular Integration guide](https://github.com/greydragon888/real-router/wiki/Angular-Integration) for Angular-specific examples and gotchas.
707
+
708
+ The shared (cross-framework) wiki pages use the `use*` naming convention — they cover every adapter (React, Preact, Solid, Vue, Svelte, Angular) and each page has an explicit Angular section showing the `inject*` form:
530
709
 
531
710
  - [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) · [Scroll Restoration](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration)
532
- - [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) · [injectRouteExit](https://github.com/greydragon888/real-router/wiki/injectRouteExit) · [injectRouteEnter](https://github.com/greydragon888/real-router/wiki/injectRouteEnter)
711
+ - [useRouter → `injectRouter`](https://github.com/greydragon888/real-router/wiki/useRouter) · [useRoute → `injectRoute`](https://github.com/greydragon888/real-router/wiki/useRoute) · [useRouteNode → `injectRouteNode`](https://github.com/greydragon888/real-router/wiki/useRouteNode) · [useNavigator → `injectNavigator`](https://github.com/greydragon888/real-router/wiki/useNavigator) · [useRouteUtils → `injectRouteUtils`](https://github.com/greydragon888/real-router/wiki/useRouteUtils) · [useRouterTransition → `injectRouterTransition`](https://github.com/greydragon888/real-router/wiki/useRouterTransition) · [useRouteExit → `injectRouteExit`](https://github.com/greydragon888/real-router/wiki/useRouteExit) · [useRouteEnter → `injectRouteEnter`](https://github.com/greydragon888/real-router/wiki/useRouteEnter)
533
712
 
534
713
  ## Related Packages
535
714