@real-router/angular 0.11.0 → 0.11.2
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/package.json +6 -7
- package/src/components/NavigationAnnouncer.ts +0 -18
- package/src/components/RouteView.ts +0 -141
- package/src/components/RouterErrorBoundary.ts +0 -72
- package/src/directives/RealLink.ts +0 -144
- package/src/directives/RealLinkActive.ts +0 -77
- package/src/directives/RouteMatch.ts +0 -7
- package/src/directives/RouteNotFound.ts +0 -6
- package/src/directives/RouteSelf.ts +0 -6
- package/src/dom-utils/direction-tracker.ts +0 -70
- package/src/dom-utils/index.ts +0 -31
- package/src/dom-utils/link-utils.ts +0 -339
- package/src/dom-utils/route-announcer.ts +0 -215
- package/src/dom-utils/scroll-restore.ts +0 -511
- package/src/dom-utils/scroll-spy.ts +0 -688
- package/src/dom-utils/view-transitions.ts +0 -142
- package/src/functions/index.ts +0 -29
- package/src/functions/injectIsActiveRoute.ts +0 -31
- package/src/functions/injectNavigator.ts +0 -12
- package/src/functions/injectOrThrow.ts +0 -19
- package/src/functions/injectRoute.ts +0 -39
- package/src/functions/injectRouteEnter.ts +0 -117
- package/src/functions/injectRouteExit.ts +0 -118
- package/src/functions/injectRouteNode.ts +0 -19
- package/src/functions/injectRouteUtils.ts +0 -15
- package/src/functions/injectRouter.ts +0 -12
- package/src/functions/injectRouterTransition.ts +0 -17
- package/src/index.ts +0 -63
- package/src/internal/buildActiveRouteOptions.ts +0 -20
- package/src/internal/install.ts +0 -90
- package/src/internal/subscribeSourceToSignal.ts +0 -48
- package/src/providers.ts +0 -80
- package/src/providersFactory.ts +0 -316
- package/src/sourceToSignal.ts +0 -28
- 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.
|
|
3
|
+
"version": "0.11.2",
|
|
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,18 +40,18 @@
|
|
|
41
40
|
"license": "MIT",
|
|
42
41
|
"sideEffects": false,
|
|
43
42
|
"dependencies": {
|
|
44
|
-
"@real-router/core": "^0.
|
|
45
|
-
"@real-router/route-utils": "^0.2.
|
|
46
|
-
"@real-router/sources": "^0.8.
|
|
43
|
+
"@real-router/core": "^0.56.0",
|
|
44
|
+
"@real-router/route-utils": "^0.2.3",
|
|
45
|
+
"@real-router/sources": "^0.8.5"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"@analogjs/vitest-angular": "2.
|
|
48
|
+
"@analogjs/vitest-angular": "2.6.0",
|
|
50
49
|
"@angular/common": "21.2.13",
|
|
51
50
|
"@angular/compiler": "21.2.13",
|
|
52
51
|
"@angular/compiler-cli": "21.2.13",
|
|
53
52
|
"@angular/core": "21.2.13",
|
|
54
53
|
"@angular/platform-browser": "21.2.13",
|
|
55
|
-
"ng-packagr": "21.2.
|
|
54
|
+
"ng-packagr": "21.2.3",
|
|
56
55
|
"typescript": "6.0.3"
|
|
57
56
|
},
|
|
58
57
|
"peerDependencies": {
|
|
@@ -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,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
|
-
}
|
package/src/dom-utils/index.ts
DELETED
|
@@ -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";
|