@real-router/angular 0.11.1 → 0.11.3

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 (38) hide show
  1. package/dist/fesm2022/real-router-angular-ssr.mjs +9 -9
  2. package/dist/fesm2022/real-router-angular.mjs +28 -27
  3. package/dist/fesm2022/real-router-angular.mjs.map +1 -1
  4. package/package.json +10 -11
  5. package/src/components/NavigationAnnouncer.ts +0 -18
  6. package/src/components/RouteView.ts +0 -141
  7. package/src/components/RouterErrorBoundary.ts +0 -72
  8. package/src/directives/RealLink.ts +0 -144
  9. package/src/directives/RealLinkActive.ts +0 -77
  10. package/src/directives/RouteMatch.ts +0 -7
  11. package/src/directives/RouteNotFound.ts +0 -6
  12. package/src/directives/RouteSelf.ts +0 -6
  13. package/src/dom-utils/direction-tracker.ts +0 -70
  14. package/src/dom-utils/index.ts +0 -31
  15. package/src/dom-utils/link-utils.ts +0 -339
  16. package/src/dom-utils/route-announcer.ts +0 -215
  17. package/src/dom-utils/scroll-restore.ts +0 -511
  18. package/src/dom-utils/scroll-spy.ts +0 -688
  19. package/src/dom-utils/view-transitions.ts +0 -142
  20. package/src/functions/index.ts +0 -29
  21. package/src/functions/injectIsActiveRoute.ts +0 -31
  22. package/src/functions/injectNavigator.ts +0 -12
  23. package/src/functions/injectOrThrow.ts +0 -19
  24. package/src/functions/injectRoute.ts +0 -39
  25. package/src/functions/injectRouteEnter.ts +0 -117
  26. package/src/functions/injectRouteExit.ts +0 -118
  27. package/src/functions/injectRouteNode.ts +0 -19
  28. package/src/functions/injectRouteUtils.ts +0 -15
  29. package/src/functions/injectRouter.ts +0 -12
  30. package/src/functions/injectRouterTransition.ts +0 -17
  31. package/src/index.ts +0 -63
  32. package/src/internal/buildActiveRouteOptions.ts +0 -20
  33. package/src/internal/install.ts +0 -90
  34. package/src/internal/subscribeSourceToSignal.ts +0 -48
  35. package/src/providers.ts +0 -80
  36. package/src/providersFactory.ts +0 -316
  37. package/src/sourceToSignal.ts +0 -28
  38. package/src/types.ts +0 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/angular",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "type": "commonjs",
5
5
  "description": "Angular 21 integration for Real-Router",
6
6
  "exports": {
@@ -17,7 +17,6 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
- "src",
21
20
  "ssr"
22
21
  ],
23
22
  "homepage": "https://github.com/greydragon888/real-router",
@@ -41,17 +40,17 @@
41
40
  "license": "MIT",
42
41
  "sideEffects": false,
43
42
  "dependencies": {
44
- "@real-router/core": "^0.55.0",
45
- "@real-router/route-utils": "^0.2.2",
46
- "@real-router/sources": "^0.8.4"
43
+ "@real-router/core": "^0.57.0",
44
+ "@real-router/route-utils": "^0.2.3",
45
+ "@real-router/sources": "^0.8.6"
47
46
  },
48
47
  "devDependencies": {
49
- "@analogjs/vitest-angular": "2.5.3",
50
- "@angular/common": "21.2.13",
51
- "@angular/compiler": "21.2.13",
52
- "@angular/compiler-cli": "21.2.13",
53
- "@angular/core": "21.2.13",
54
- "@angular/platform-browser": "21.2.13",
48
+ "@analogjs/vitest-angular": "2.6.1",
49
+ "@angular/common": "21.2.17",
50
+ "@angular/compiler": "21.2.17",
51
+ "@angular/compiler-cli": "21.2.17",
52
+ "@angular/core": "21.2.17",
53
+ "@angular/platform-browser": "21.2.17",
55
54
  "ng-packagr": "21.2.3",
56
55
  "typescript": "6.0.3"
57
56
  },
@@ -1,18 +0,0 @@
1
- import { Component, inject, DestroyRef } from "@angular/core";
2
-
3
- import { createRouteAnnouncer } from "../dom-utils";
4
- import { injectRouter } from "../functions/injectRouter";
5
-
6
- @Component({
7
- selector: "navigation-announcer",
8
- template: "",
9
- })
10
- export class NavigationAnnouncer {
11
- private readonly announcer = createRouteAnnouncer(injectRouter());
12
-
13
- constructor() {
14
- inject(DestroyRef).onDestroy(() => {
15
- this.announcer.destroy();
16
- });
17
- }
18
- }
@@ -1,141 +0,0 @@
1
- import { NgTemplateOutlet } from "@angular/common";
2
- import {
3
- Component,
4
- computed,
5
- contentChildren,
6
- effect,
7
- input,
8
- signal,
9
- type TemplateRef,
10
- } from "@angular/core";
11
- import { UNKNOWN_ROUTE } from "@real-router/core";
12
- import { startsWithSegment } from "@real-router/route-utils";
13
- import { createRouteNodeSource } from "@real-router/sources";
14
-
15
- import { RouteMatch } from "../directives/RouteMatch";
16
- import { RouteNotFound } from "../directives/RouteNotFound";
17
- import { RouteSelf } from "../directives/RouteSelf";
18
- import { injectRouter } from "../functions/injectRouter";
19
- import { subscribeSourceToSignal } from "../internal/subscribeSourceToSignal";
20
-
21
- import type { RouteSnapshot } from "@real-router/sources";
22
-
23
- const EMPTY_SNAPSHOT: RouteSnapshot = {
24
- route: undefined,
25
- previousRoute: undefined,
26
- };
27
-
28
- @Component({
29
- selector: "route-view",
30
- template: `
31
- @if (activeTemplate()) {
32
- <ng-container [ngTemplateOutlet]="activeTemplate()!" />
33
- }
34
- `,
35
- imports: [NgTemplateOutlet],
36
- })
37
- export class RouteView {
38
- readonly nodeName = input<string>("", { alias: "routeNode" });
39
-
40
- readonly matches = contentChildren(RouteMatch, { descendants: true });
41
- readonly selfs = contentChildren(RouteSelf, { descendants: true });
42
- readonly notFounds = contentChildren(RouteNotFound, { descendants: true });
43
-
44
- readonly activeTemplate = computed<TemplateRef<unknown> | null>(
45
- () => this.matchedTemplate() ?? this.fallbackTemplate(),
46
- );
47
-
48
- private readonly router = injectRouter();
49
- private readonly routeState = signal<RouteSnapshot>(EMPTY_SNAPSHOT);
50
-
51
- private readonly matchEntries = computed(() => {
52
- const nodeName = this.nodeName();
53
-
54
- return this.matches().map((match) => {
55
- const segment = match.routeMatch();
56
-
57
- return {
58
- match,
59
- fullSegmentName: nodeName ? `${nodeName}.${segment}` : segment,
60
- };
61
- });
62
- });
63
-
64
- // The matched template (Match priority) is independent of the Self /
65
- // NotFound fallback chain. Splitting the two paths into separate computeds
66
- // localises re-runs: a change to `selfs()` / `notFounds()` no longer
67
- // re-evaluates the Match loop (review §8a LOW — RouteView activeTemplate
68
- // split).
69
- private readonly matchedTemplate = computed<TemplateRef<unknown> | null>(
70
- () => {
71
- const route = this.routeState().route;
72
-
73
- if (!route) {
74
- return null;
75
- }
76
-
77
- const routeName = route.name;
78
-
79
- for (const { match, fullSegmentName } of this.matchEntries()) {
80
- if (startsWithSegment(routeName, fullSegmentName)) {
81
- return match.templateRef;
82
- }
83
- }
84
-
85
- return null;
86
- },
87
- );
88
-
89
- // Fallback chain — only consulted when `matchedTemplate()` returned `null`.
90
- // Template priority: Self → NotFound. Selection rules differ on purpose:
91
- // - **Self uses first-wins** (`.at(0)`) for parity with React / Preact /
92
- // Solid / Vue, where the first matching `<Self>` token in declaration
93
- // order wins.
94
- // - **NotFound uses last-wins** (`.at(-1)`) intentionally — the fallback
95
- // should be the most-recently-declared template so that consumers can
96
- // override an inherited `<ng-template routeNotFound>` simply by
97
- // re-declaring it lower in the projected content.
98
- private readonly fallbackTemplate = computed<TemplateRef<unknown> | null>(
99
- () => {
100
- const route = this.routeState().route;
101
-
102
- if (!route) {
103
- return null;
104
- }
105
-
106
- const routeName = route.name;
107
-
108
- if (routeName === this.nodeName()) {
109
- const first = this.selfs().at(0);
110
-
111
- if (first) {
112
- return first.templateRef;
113
- }
114
- }
115
-
116
- if (routeName === UNKNOWN_ROUTE) {
117
- const last = this.notFounds().at(-1);
118
-
119
- if (last) {
120
- return last.templateRef;
121
- }
122
- }
123
-
124
- return null;
125
- },
126
- );
127
-
128
- constructor() {
129
- // Reactive source-creation effect (#630 fix) — see
130
- // `packages/angular/CLAUDE.md` → "Directives use constructor + effect()".
131
- effect((onCleanup) => {
132
- const source = createRouteNodeSource(this.router, this.nodeName());
133
-
134
- onCleanup(
135
- subscribeSourceToSignal(source, (snap) => {
136
- this.routeState.set(snap);
137
- }),
138
- );
139
- });
140
- }
141
- }
@@ -1,72 +0,0 @@
1
- import { NgTemplateOutlet } from "@angular/common";
2
- import { Component, computed, effect, input, output } from "@angular/core";
3
- import { createDismissableError } from "@real-router/sources";
4
-
5
- import { injectRouter } from "../functions/injectRouter";
6
- import { sourceToSignal } from "../sourceToSignal";
7
-
8
- import type { ErrorContext } from "../types";
9
- import type { TemplateRef } from "@angular/core";
10
- import type { RouterError, State } from "@real-router/core";
11
- import type { DismissableErrorSnapshot } from "@real-router/sources";
12
-
13
- @Component({
14
- selector: "router-error-boundary",
15
- template: `
16
- <ng-content />
17
- @if (errorContext() && errorTemplate()) {
18
- <ng-container
19
- [ngTemplateOutlet]="errorTemplate()!"
20
- [ngTemplateOutletContext]="errorContext()!"
21
- />
22
- }
23
- `,
24
- imports: [NgTemplateOutlet],
25
- })
26
- export class RouterErrorBoundary {
27
- readonly errorTemplate = input<TemplateRef<ErrorContext>>();
28
-
29
- readonly onError = output<{
30
- error: RouterError;
31
- toRoute: State | null;
32
- fromRoute: State | null;
33
- }>();
34
-
35
- readonly errorContext = computed<ErrorContext | null>(() => {
36
- const snap = this.snapshot();
37
-
38
- if (!snap.error) {
39
- return null;
40
- }
41
-
42
- return {
43
- $implicit: snap.error,
44
- resetError: snap.resetError,
45
- };
46
- });
47
-
48
- private readonly router = injectRouter();
49
- private readonly snapshot = sourceToSignal<DismissableErrorSnapshot>(
50
- createDismissableError(this.router),
51
- );
52
-
53
- constructor() {
54
- // `effect()` registers itself with the current injection context's
55
- // `DestroyRef` and tears down automatically when the component is
56
- // destroyed. The earlier manual `effectRef.destroy()` wired through
57
- // `inject(DestroyRef).onDestroy(...)` duplicated that built-in cleanup
58
- // (audit §8.1 LOW — confirmed: no behavior change without the manual
59
- // path).
60
- effect(() => {
61
- const snap = this.snapshot();
62
-
63
- if (snap.error) {
64
- this.onError.emit({
65
- error: snap.error,
66
- toRoute: snap.toRoute,
67
- fromRoute: snap.fromRoute,
68
- });
69
- }
70
- });
71
- }
72
- }
@@ -1,144 +0,0 @@
1
- import {
2
- Directive,
3
- ElementRef,
4
- computed,
5
- effect,
6
- inject,
7
- input,
8
- signal,
9
- } from "@angular/core";
10
- import { createActiveRouteSource } from "@real-router/sources";
11
-
12
- import { buildHref, navigateWithHash, shouldNavigate } from "../dom-utils";
13
- import { injectRouter } from "../functions/injectRouter";
14
- import { buildActiveRouteOptions } from "../internal/buildActiveRouteOptions";
15
- import { subscribeSourceToSignal } from "../internal/subscribeSourceToSignal";
16
-
17
- import type { Params, NavigationOptions } from "@real-router/core";
18
-
19
- const NOOP_CATCH = (): void => {};
20
-
21
- @Directive({
22
- selector: "a[realLink]",
23
- host: {
24
- "(click)": "onClick($event)",
25
- },
26
- })
27
- export class RealLink {
28
- readonly routeName = input<string>("");
29
- readonly routeParams = input<Params>({});
30
- readonly routeOptions = input<NavigationOptions>({});
31
- readonly activeClassName = input<string>("active");
32
- readonly activeStrict = input(false);
33
- readonly ignoreQueryParams = input(true);
34
- /**
35
- * URL fragment (decoded form, no leading "#") (#532).
36
- * - omitted/`undefined` → preserve current fragment on same-route navigation
37
- * - `""` → clear fragment
38
- * - non-empty → set fragment
39
- */
40
- readonly hash = input<string | undefined>(undefined);
41
-
42
- private readonly router = injectRouter();
43
- private readonly anchor = inject(ElementRef)
44
- .nativeElement as HTMLAnchorElement;
45
- private readonly isActive = signal(false);
46
- // `href` is computed from signal inputs only — Angular's default Object.is
47
- // equality already collapses repeated `string` results, so no custom
48
- // comparator is required (review §8b note 3 — applies after verifying that
49
- // `buildHref` returns a primitive).
50
- private readonly href = computed(() => {
51
- const hashValue = this.hash();
52
-
53
- return buildHref(
54
- this.router,
55
- this.routeName(),
56
- this.routeParams(),
57
- hashValue === undefined ? undefined : { hash: hashValue },
58
- );
59
- });
60
- private prevActiveClass = "";
61
- private prevHref: string | undefined = undefined;
62
- // Skip-same-value: only re-touch the DOM `class` list when the active state
63
- // actually flipped. Without this, every navigation that re-fires the active
64
- // source still issues a `classList.toggle` no-op (review §8b MEDIUM).
65
- private prevActive: boolean | undefined = undefined;
66
-
67
- constructor() {
68
- // Reactive source-creation effect (#630 fix) — see
69
- // `packages/angular/CLAUDE.md` → "Directives use constructor + effect()".
70
- // Reading signal inputs inside `effect()` re-creates the active-route
71
- // source whenever any input changes; `onCleanup` tears the previous
72
- // subscription down.
73
- effect((onCleanup) => {
74
- const source = createActiveRouteSource(
75
- this.router,
76
- this.routeName(),
77
- this.routeParams(),
78
- buildActiveRouteOptions(
79
- this.activeStrict(),
80
- this.ignoreQueryParams(),
81
- this.hash(),
82
- ),
83
- );
84
-
85
- onCleanup(
86
- subscribeSourceToSignal(source, (snap) => {
87
- // Pure-href refresh: when the active flag did not change, only the
88
- // href may have moved (e.g. param-only update on a parent route).
89
- // Skip the classList work in that branch (review §8b MEDIUM).
90
- if (snap === this.prevActive) {
91
- this.isActive.set(snap);
92
- this.updateHref();
93
-
94
- return;
95
- }
96
-
97
- this.prevActive = snap;
98
- this.isActive.set(snap);
99
- this.updateHref();
100
- this.updateActiveClass();
101
- }),
102
- );
103
- });
104
- }
105
-
106
- onClick(event: MouseEvent): void {
107
- if (!shouldNavigate(event) || this.anchor.target === "_blank") {
108
- return;
109
- }
110
-
111
- event.preventDefault();
112
- navigateWithHash(
113
- this.router,
114
- this.routeName(),
115
- this.routeParams(),
116
- this.hash(),
117
- this.routeOptions(),
118
- ).catch(NOOP_CATCH);
119
- }
120
-
121
- private updateHref(): void {
122
- const href = this.href();
123
-
124
- if (href !== undefined && href !== this.prevHref) {
125
- this.anchor.setAttribute("href", href);
126
- }
127
-
128
- this.prevHref = href;
129
- }
130
-
131
- private updateActiveClass(): void {
132
- const activeClass = this.activeClassName();
133
-
134
- if (this.prevActiveClass && this.prevActiveClass !== activeClass) {
135
- this.anchor.classList.remove(this.prevActiveClass);
136
- }
137
-
138
- if (activeClass) {
139
- this.anchor.classList.toggle(activeClass, this.isActive());
140
- }
141
-
142
- this.prevActiveClass = activeClass;
143
- }
144
- }
@@ -1,77 +0,0 @@
1
- import {
2
- Directive,
3
- ElementRef,
4
- effect,
5
- inject,
6
- input,
7
- signal,
8
- } from "@angular/core";
9
- import { createActiveRouteSource } from "@real-router/sources";
10
-
11
- import { applyLinkA11y } from "../dom-utils";
12
- import { injectRouter } from "../functions/injectRouter";
13
- import { buildActiveRouteOptions } from "../internal/buildActiveRouteOptions";
14
- import { subscribeSourceToSignal } from "../internal/subscribeSourceToSignal";
15
-
16
- import type { Params } from "@real-router/core";
17
-
18
- @Directive({ selector: "[realLinkActive]" })
19
- export class RealLinkActive {
20
- readonly realLinkActive = input<string>("");
21
- readonly routeName = input<string>("");
22
- readonly routeParams = input<Params>({});
23
- readonly activeStrict = input(false);
24
- readonly ignoreQueryParams = input(true);
25
-
26
- private readonly router = injectRouter();
27
- private readonly element = inject(ElementRef).nativeElement as HTMLElement;
28
- private readonly isActive = signal(false);
29
- // Skip-same-value: only touch `classList.toggle` when the active flag
30
- // actually flipped. Saves one DOM write per RealLinkActive per unrelated
31
- // navigation (review §8b MEDIUM).
32
- private prevActive: boolean | undefined = undefined;
33
-
34
- constructor() {
35
- // One-time a11y setup — doesn't depend on signal inputs, stays in
36
- // constructor body. `applyLinkA11y` is idempotent so re-running would
37
- // be safe, but we only need it once per element.
38
- applyLinkA11y(this.element);
39
-
40
- // Reactive source-creation effect (#630 fix) — see
41
- // `packages/angular/CLAUDE.md` → "Directives use constructor + effect()".
42
- effect((onCleanup) => {
43
- const source = createActiveRouteSource(
44
- this.router,
45
- this.routeName(),
46
- this.routeParams(),
47
- buildActiveRouteOptions(
48
- this.activeStrict(),
49
- this.ignoreQueryParams(),
50
- undefined,
51
- ),
52
- );
53
-
54
- onCleanup(
55
- subscribeSourceToSignal(source, (snap) => {
56
- if (snap === this.prevActive) {
57
- return;
58
- }
59
-
60
- this.prevActive = snap;
61
- this.isActive.set(snap);
62
- this.updateClass();
63
- }),
64
- );
65
- });
66
- }
67
-
68
- private updateClass(): void {
69
- const className = this.realLinkActive();
70
-
71
- if (!className) {
72
- return;
73
- }
74
-
75
- this.element.classList.toggle(className, this.isActive());
76
- }
77
- }
@@ -1,7 +0,0 @@
1
- import { Directive, TemplateRef, inject, input } from "@angular/core";
2
-
3
- @Directive({ selector: "ng-template[routeMatch]" })
4
- export class RouteMatch {
5
- readonly routeMatch = input.required<string>();
6
- readonly templateRef = inject(TemplateRef);
7
- }
@@ -1,6 +0,0 @@
1
- import { Directive, TemplateRef, inject } from "@angular/core";
2
-
3
- @Directive({ selector: "ng-template[routeNotFound]" })
4
- export class RouteNotFound {
5
- readonly templateRef = inject(TemplateRef);
6
- }
@@ -1,6 +0,0 @@
1
- import { Directive, TemplateRef, inject } from "@angular/core";
2
-
3
- @Directive({ selector: "ng-template[routeSelf]" })
4
- export class RouteSelf {
5
- readonly templateRef = inject(TemplateRef);
6
- }
@@ -1,70 +0,0 @@
1
- import type { Router } from "@real-router/core";
2
-
3
- export interface DirectionTracker {
4
- destroy: () => void;
5
- }
6
-
7
- const NOOP_INSTANCE: DirectionTracker = Object.freeze({
8
- destroy: () => {
9
- /* no-op */
10
- },
11
- });
12
-
13
- /**
14
- * Track navigation direction (forward / back) and write it to
15
- * `<html data-nav-direction>` on every leave. CSS / JS readers consume
16
- * the attribute via `html[data-nav-direction="back"]` selectors or
17
- * `document.documentElement.dataset.navDirection`.
18
- *
19
- * Mechanism-agnostic — works identically whether downstream UI uses CSS
20
- * `@keyframes`, View Transitions pseudo-elements, or library state
21
- * (motion's `motion.div initial={{ x: ... }}`).
22
- *
23
- * Implementation:
24
- * - On install, set `data-nav-direction="forward"` baseline.
25
- * - Attach a `popstate` listener that flips an internal flag to
26
- * `true`. Browser back/forward navigation triggers popstate; user
27
- * clicks on `<Link>` / programmatic `router.navigate(...)` do not.
28
- * - On every `subscribeLeave`, write
29
- * `popstateFlag ? "back" : "forward"` and reset the flag.
30
- *
31
- * Returns `{ destroy }` to clean up the listener and clear the dataset
32
- * attribute.
33
- */
34
- export function createDirectionTracker(router: Router): DirectionTracker {
35
- if (typeof document === "undefined") {
36
- return NOOP_INSTANCE;
37
- }
38
-
39
- let popstateFlag = false;
40
-
41
- document.documentElement.dataset.navDirection = "forward";
42
-
43
- const onPopstate = (): void => {
44
- popstateFlag = true;
45
- };
46
-
47
- // IMPORTANT — listener-ordering: `popstate` fires on `window`, which
48
- // has no DOM descendants, so capture phase is moot. Listeners are
49
- // dispatched in registration order. To beat the browser-plugin's own
50
- // popstate handler, this tracker must be installed **before**
51
- // `router.usePlugin(browserPluginFactory())` in user code. Otherwise
52
- // the plugin's handler runs first and synchronously fires
53
- // `subscribeLeave` while `popstateFlag` is still `false`.
54
- globalThis.addEventListener("popstate", onPopstate);
55
-
56
- const offLeave = router.subscribeLeave(() => {
57
- document.documentElement.dataset.navDirection = popstateFlag
58
- ? "back"
59
- : "forward";
60
- popstateFlag = false;
61
- });
62
-
63
- return {
64
- destroy: () => {
65
- offLeave();
66
- globalThis.removeEventListener("popstate", onPopstate);
67
- delete document.documentElement.dataset.navDirection;
68
- },
69
- };
70
- }
@@ -1,31 +0,0 @@
1
- export { createDirectionTracker } from "./direction-tracker";
2
-
3
- export { createRouteAnnouncer } from "./route-announcer";
4
-
5
- export { createScrollRestoration } from "./scroll-restore";
6
-
7
- export { createScrollSpy } from "./scroll-spy";
8
-
9
- export { createViewTransitions } from "./view-transitions";
10
-
11
- export {
12
- shouldNavigate,
13
- buildHref,
14
- buildActiveClassName,
15
- navigateWithHash,
16
- shallowEqual,
17
- applyLinkA11y,
18
- } from "./link-utils";
19
-
20
- export type { RouteAnnouncerOptions } from "./route-announcer";
21
-
22
- export type {
23
- ScrollRestorationOptions,
24
- ScrollRestorationMode,
25
- } from "./scroll-restore";
26
-
27
- export type { ScrollSpy, ScrollSpyOptions } from "./scroll-spy";
28
-
29
- export type { DirectionTracker } from "./direction-tracker";
30
-
31
- export type { ViewTransitions } from "./view-transitions";