@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/README.md +454 -0
- package/dist/README.md +454 -0
- package/dist/fesm2022/real-router-angular.mjs +575 -0
- package/dist/fesm2022/real-router-angular.mjs.map +1 -0
- package/dist/types/real-router-angular.d.ts +134 -0
- package/dist/types/real-router-angular.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/components/NavigationAnnouncer.ts +18 -0
- package/src/components/RouteView.ts +102 -0
- package/src/components/RouterErrorBoundary.ts +88 -0
- package/src/directives/RealLink.ts +97 -0
- package/src/directives/RealLinkActive.ts +68 -0
- package/src/directives/RouteMatch.ts +7 -0
- package/src/directives/RouteNotFound.ts +6 -0
- package/src/dom-utils/CLAUDE.md +76 -0
- package/src/dom-utils/index.ts +11 -0
- package/src/dom-utils/link-utils.ts +121 -0
- package/src/dom-utils/route-announcer.ts +159 -0
- package/src/functions/index.ts +13 -0
- package/src/functions/injectIsActiveRoute.ts +21 -0
- package/src/functions/injectNavigator.ts +8 -0
- package/src/functions/injectOrThrow.ts +15 -0
- package/src/functions/injectRoute.ts +8 -0
- package/src/functions/injectRouteNode.ts +16 -0
- package/src/functions/injectRouteUtils.ts +12 -0
- package/src/functions/injectRouter.ts +8 -0
- package/src/functions/injectRouterTransition.ts +14 -0
- package/src/index.ts +39 -0
- package/src/providers.ts +33 -0
- package/src/sourceToSignal.ts +20 -0
- package/src/types.ts +8 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
ElementRef,
|
|
4
|
+
inject,
|
|
5
|
+
input,
|
|
6
|
+
signal,
|
|
7
|
+
DestroyRef,
|
|
8
|
+
type OnInit,
|
|
9
|
+
} from "@angular/core";
|
|
10
|
+
import { createActiveRouteSource } from "@real-router/sources";
|
|
11
|
+
|
|
12
|
+
import { applyLinkA11y } from "../dom-utils";
|
|
13
|
+
import { injectRouter } from "../functions/injectRouter";
|
|
14
|
+
|
|
15
|
+
import type { Params } from "@real-router/core";
|
|
16
|
+
|
|
17
|
+
@Directive({ selector: "[realLinkActive]" })
|
|
18
|
+
export class RealLinkActive implements OnInit {
|
|
19
|
+
readonly realLinkActive = input<string>("");
|
|
20
|
+
readonly routeName = input<string>("");
|
|
21
|
+
readonly routeParams = input<Params>({});
|
|
22
|
+
readonly activeStrict = input(false);
|
|
23
|
+
readonly ignoreQueryParams = input(true);
|
|
24
|
+
|
|
25
|
+
private readonly router = injectRouter();
|
|
26
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
27
|
+
private readonly element = inject(ElementRef).nativeElement as HTMLElement;
|
|
28
|
+
private readonly isActive = signal(false);
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
applyLinkA11y(this.element);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ngOnInit(): void {
|
|
35
|
+
const source = createActiveRouteSource(
|
|
36
|
+
this.router,
|
|
37
|
+
this.routeName(),
|
|
38
|
+
this.routeParams(),
|
|
39
|
+
{
|
|
40
|
+
strict: this.activeStrict(),
|
|
41
|
+
ignoreQueryParams: this.ignoreQueryParams(),
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
this.isActive.set(source.getSnapshot());
|
|
46
|
+
this.updateClass();
|
|
47
|
+
|
|
48
|
+
const unsub = source.subscribe(() => {
|
|
49
|
+
this.isActive.set(source.getSnapshot());
|
|
50
|
+
this.updateClass();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.destroyRef.onDestroy(() => {
|
|
54
|
+
unsub();
|
|
55
|
+
source.destroy();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private updateClass(): void {
|
|
60
|
+
const className = this.realLinkActive();
|
|
61
|
+
|
|
62
|
+
if (!className) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.element.classList.toggle(className, this.isActive());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# shared/dom-utils — Stable Shared Sources
|
|
2
|
+
|
|
3
|
+
> DOM helpers инлайнятся во все framework-адаптеры. Код **зрелый**, API устоялся.
|
|
4
|
+
|
|
5
|
+
## Status: stable
|
|
6
|
+
|
|
7
|
+
Эти файлы — общий слой для 6 адаптеров (Preact, React, Solid, Vue, Svelte, Angular). Любое изменение здесь немедленно попадает в 6 публичных bundle'ов. Относиться как к low-level примитиву, а не к «просто утилитам».
|
|
8
|
+
|
|
9
|
+
## Правила работы
|
|
10
|
+
|
|
11
|
+
### 1. Отладка начинается в адаптере, а не здесь
|
|
12
|
+
|
|
13
|
+
Перед правкой `shared/dom-utils/*.ts` убедись, что:
|
|
14
|
+
- баг воспроизводится **минимум в 2 адаптерах** (иначе это adapter-specific проблема обёртки);
|
|
15
|
+
- есть падающий тест в функциональном наборе соответствующего адаптера.
|
|
16
|
+
|
|
17
|
+
Если баг живёт только в одном адаптере — правка идёт в адаптер, не сюда.
|
|
18
|
+
|
|
19
|
+
### 2. Оптимизаций «на глаз» не делать
|
|
20
|
+
|
|
21
|
+
Функции уже прошли итерации:
|
|
22
|
+
- `buildActiveClassName` — токен-дедупликация через `Set`, O(n+m);
|
|
23
|
+
- `applyLinkA11y` — defensive null-guard + `hasAttribute` (не `getAttribute`);
|
|
24
|
+
- `buildHref` — optional `buildUrl` + undefined-fallback на `buildPath`;
|
|
25
|
+
- `createRouteAnnouncer` — double `requestAnimationFrame` + Safari-ready буферизация через `pendingText`.
|
|
26
|
+
|
|
27
|
+
«Рефактор ради чистоты» здесь запрещён — микро-изменение ломает сразу 6 bundle'ов. Изменение кода требует: конкретный баг/юз-кейс, тест на него, прогон `pnpm build`.
|
|
28
|
+
|
|
29
|
+
### 3. Angular sync после любой правки
|
|
30
|
+
|
|
31
|
+
`packages/angular/src/dom-utils/` — **git-tracked copy**, не symlink (ng-packagr не следует за symlinks как tsdown).
|
|
32
|
+
|
|
33
|
+
После правки `shared/dom-utils/*.ts`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm -F @real-router/angular bundle # пере-материализует копию через prebundle
|
|
37
|
+
diff -r shared/dom-utils/ packages/angular/src/dom-utils/ | grep -v index.ts # должно быть пусто
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Коммитить нужно **оба** варианта: источник в `shared/` и копию в `packages/angular/src/dom-utils/`.
|
|
41
|
+
|
|
42
|
+
### 4. Валидация изменений
|
|
43
|
+
|
|
44
|
+
Минимальный чек-лист перед коммитом:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm build # 207+ тасков, все адаптеры
|
|
48
|
+
pnpm -F @real-router/react test:properties -- --run # property-тесты buildActiveClassName/buildHref
|
|
49
|
+
pnpm -F @real-router/svelte test:properties -- --run # property-тесты (дублируют React, оба должны пройти)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Консумеры
|
|
53
|
+
|
|
54
|
+
| Источник | Как подключено | Пакеты |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `shared/dom-utils/` | symlink → `src/dom-utils` | `preact`, `react`, `solid`, `svelte`, `vue` |
|
|
57
|
+
| `shared/dom-utils/` | git-tracked copy (через `prebundle`) | `angular` |
|
|
58
|
+
|
|
59
|
+
Проверка, что symlink цел: `readlink packages/preact/src/dom-utils` должен вернуть относительный путь к `shared/dom-utils/`.
|
|
60
|
+
|
|
61
|
+
## Публичный контракт
|
|
62
|
+
|
|
63
|
+
| Функция | Используется в | Стабильность |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `shouldNavigate(evt)` | все `Link` компоненты + directives/actions | frozen |
|
|
66
|
+
| `buildHref(router, name, params)` | все `Link` компоненты + Angular directive | frozen |
|
|
67
|
+
| `buildActiveClassName(isActive, active, base)` | все `Link` компоненты | frozen |
|
|
68
|
+
| `applyLinkA11y(el)` | все `Link` компоненты + directives | frozen |
|
|
69
|
+
| `createRouteAnnouncer(router, opts)` | все `RouterProvider` / `NavigationAnnouncer` | frozen |
|
|
70
|
+
|
|
71
|
+
«Frozen» = сигнатура и поведение не меняются без мажорного release-цикла core. Новые фичи — отдельными функциями, не модификацией существующих.
|
|
72
|
+
|
|
73
|
+
## См. также
|
|
74
|
+
|
|
75
|
+
- [../../CLAUDE.md](../../CLAUDE.md) — monorepo-level правила symlink-инфраструктуры
|
|
76
|
+
- [../../IMPLEMENTATION_NOTES.md](../../IMPLEMENTATION_NOTES.md) — раздел "Shared Sources via Symlinks"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Router, Params } from "@real-router/core";
|
|
2
|
+
|
|
3
|
+
export function shouldNavigate(evt: MouseEvent): boolean {
|
|
4
|
+
return (
|
|
5
|
+
evt.button === 0 &&
|
|
6
|
+
!evt.metaKey &&
|
|
7
|
+
!evt.altKey &&
|
|
8
|
+
!evt.ctrlKey &&
|
|
9
|
+
!evt.shiftKey
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type BuildUrlFn = (name: string, params: Params) => string | undefined;
|
|
14
|
+
|
|
15
|
+
export function buildHref(
|
|
16
|
+
router: Router,
|
|
17
|
+
routeName: string,
|
|
18
|
+
routeParams: Params,
|
|
19
|
+
): string | undefined {
|
|
20
|
+
try {
|
|
21
|
+
const buildUrl = router.buildUrl as BuildUrlFn | undefined;
|
|
22
|
+
|
|
23
|
+
if (buildUrl) {
|
|
24
|
+
const url = buildUrl(routeName, routeParams);
|
|
25
|
+
|
|
26
|
+
if (url !== undefined) {
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return router.buildPath(routeName, routeParams);
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(
|
|
34
|
+
`[real-router] Route "${routeName}" is not defined. The element will render without an href attribute.`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseTokens(value: string | undefined): string[] {
|
|
42
|
+
return value ? (value.match(/\S+/g) ?? []) : [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildActiveClassName(
|
|
46
|
+
isActive: boolean,
|
|
47
|
+
activeClassName: string | undefined,
|
|
48
|
+
baseClassName: string | undefined,
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (isActive && activeClassName) {
|
|
51
|
+
const activeTokens = parseTokens(activeClassName);
|
|
52
|
+
|
|
53
|
+
if (activeTokens.length === 0) {
|
|
54
|
+
return baseClassName ?? undefined;
|
|
55
|
+
}
|
|
56
|
+
if (!baseClassName) {
|
|
57
|
+
return activeTokens.join(" ");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const baseTokens = parseTokens(baseClassName);
|
|
61
|
+
const seen = new Set(baseTokens);
|
|
62
|
+
|
|
63
|
+
for (const token of activeTokens) {
|
|
64
|
+
if (!seen.has(token)) {
|
|
65
|
+
seen.add(token);
|
|
66
|
+
baseTokens.push(token);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return baseTokens.join(" ");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return baseClassName ?? undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function shallowEqual(
|
|
77
|
+
prev: object | undefined,
|
|
78
|
+
next: object | undefined,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (Object.is(prev, next)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (!prev || !next) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const prevKeys = Object.keys(prev);
|
|
88
|
+
|
|
89
|
+
if (prevKeys.length !== Object.keys(next).length) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const prevRecord = prev as Record<string, unknown>;
|
|
94
|
+
const nextRecord = next as Record<string, unknown>;
|
|
95
|
+
|
|
96
|
+
for (const key of prevKeys) {
|
|
97
|
+
if (!Object.is(prevRecord[key], nextRecord[key])) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function applyLinkA11y(element: HTMLElement | null | undefined): void {
|
|
106
|
+
if (!element) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
element instanceof HTMLAnchorElement ||
|
|
111
|
+
element instanceof HTMLButtonElement
|
|
112
|
+
) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!element.hasAttribute("role")) {
|
|
116
|
+
element.setAttribute("role", "link");
|
|
117
|
+
}
|
|
118
|
+
if (!element.hasAttribute("tabindex")) {
|
|
119
|
+
element.setAttribute("tabindex", "0");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { Router, State } from "@real-router/core";
|
|
2
|
+
|
|
3
|
+
const CLEAR_DELAY = 7000;
|
|
4
|
+
const SAFARI_READY_DELAY = 100;
|
|
5
|
+
const ANNOUNCER_ATTR = "data-real-router-announcer";
|
|
6
|
+
const INTERNAL_ROUTE_PREFIX = "@@";
|
|
7
|
+
const VISUALLY_HIDDEN =
|
|
8
|
+
"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0";
|
|
9
|
+
|
|
10
|
+
export interface RouteAnnouncerOptions {
|
|
11
|
+
prefix?: string;
|
|
12
|
+
getAnnouncementText?: (route: State) => string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createRouteAnnouncer(
|
|
16
|
+
router: Router,
|
|
17
|
+
options?: RouteAnnouncerOptions,
|
|
18
|
+
): { destroy: () => void } {
|
|
19
|
+
const prefix = options?.prefix ?? "Navigated to ";
|
|
20
|
+
const getCustomText = options?.getAnnouncementText;
|
|
21
|
+
|
|
22
|
+
let isInitialNavigation = true;
|
|
23
|
+
let isReady = false;
|
|
24
|
+
let isDestroyed = false;
|
|
25
|
+
let lastAnnouncedText = "";
|
|
26
|
+
let pendingText: string | null = null;
|
|
27
|
+
let clearTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
28
|
+
|
|
29
|
+
const announcer = getOrCreateAnnouncer();
|
|
30
|
+
|
|
31
|
+
const doAnnounce = (text: string, h1: HTMLElement | null): void => {
|
|
32
|
+
lastAnnouncedText = text;
|
|
33
|
+
clearTimeout(clearTimeoutId);
|
|
34
|
+
announcer.textContent = text;
|
|
35
|
+
clearTimeoutId = setTimeout(() => {
|
|
36
|
+
announcer.textContent = "";
|
|
37
|
+
lastAnnouncedText = "";
|
|
38
|
+
}, CLEAR_DELAY);
|
|
39
|
+
|
|
40
|
+
manageFocus(h1);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Safari-ready delay: announcing before VoiceOver wires up the aria-live region
|
|
44
|
+
// causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms
|
|
45
|
+
// before marking the announcer "ready" — any navigation during that window is
|
|
46
|
+
// buffered in pendingText and flushed once the delay expires.
|
|
47
|
+
const safariTimeoutId = setTimeout(() => {
|
|
48
|
+
isReady = true;
|
|
49
|
+
|
|
50
|
+
if (pendingText !== null && !isDestroyed) {
|
|
51
|
+
const text = pendingText;
|
|
52
|
+
|
|
53
|
+
pendingText = null;
|
|
54
|
+
doAnnounce(text, document.querySelector<HTMLElement>("h1"));
|
|
55
|
+
}
|
|
56
|
+
}, SAFARI_READY_DELAY);
|
|
57
|
+
|
|
58
|
+
const unsubscribe = router.subscribe(({ route }) => {
|
|
59
|
+
if (isInitialNavigation) {
|
|
60
|
+
isInitialNavigation = false;
|
|
61
|
+
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Double rAF: waits for two paint frames so the incoming route's DOM
|
|
66
|
+
// (including the new <h1>) is fully rendered before resolveText reads it.
|
|
67
|
+
// Single rAF fires before the new route's template has been attached,
|
|
68
|
+
// which would cause resolveText to pick up the OLD h1 or fall back to
|
|
69
|
+
// document.title / route.name prematurely.
|
|
70
|
+
requestAnimationFrame(() => {
|
|
71
|
+
requestAnimationFrame(() => {
|
|
72
|
+
if (isDestroyed) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const h1 = document.querySelector<HTMLElement>("h1");
|
|
77
|
+
const text = resolveText(route, prefix, getCustomText, h1);
|
|
78
|
+
|
|
79
|
+
if (!text || text === lastAnnouncedText) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!isReady) {
|
|
84
|
+
// Defer announcement until Safari-ready window elapses (see safariTimeoutId).
|
|
85
|
+
pendingText = text;
|
|
86
|
+
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
doAnnounce(text, h1);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
destroy() {
|
|
97
|
+
isDestroyed = true;
|
|
98
|
+
unsubscribe();
|
|
99
|
+
clearTimeout(clearTimeoutId);
|
|
100
|
+
clearTimeout(safariTimeoutId);
|
|
101
|
+
removeAnnouncer();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getOrCreateAnnouncer(): HTMLElement {
|
|
107
|
+
const existing = document.querySelector<HTMLElement>(`[${ANNOUNCER_ATTR}]`);
|
|
108
|
+
|
|
109
|
+
if (existing) {
|
|
110
|
+
return existing;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const element = document.createElement("div");
|
|
114
|
+
|
|
115
|
+
element.setAttribute("style", VISUALLY_HIDDEN);
|
|
116
|
+
element.setAttribute("aria-live", "assertive");
|
|
117
|
+
element.setAttribute("aria-atomic", "true");
|
|
118
|
+
element.setAttribute(ANNOUNCER_ATTR, "");
|
|
119
|
+
|
|
120
|
+
document.body.prepend(element);
|
|
121
|
+
|
|
122
|
+
return element;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function removeAnnouncer(): void {
|
|
126
|
+
document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveText(
|
|
130
|
+
route: State,
|
|
131
|
+
prefix: string,
|
|
132
|
+
getCustomText: ((route: State) => string) | undefined,
|
|
133
|
+
h1: HTMLElement | null,
|
|
134
|
+
): string {
|
|
135
|
+
if (getCustomText) {
|
|
136
|
+
return getCustomText(route);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const h1Text = h1?.textContent.trim() ?? "";
|
|
140
|
+
const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)
|
|
141
|
+
? ""
|
|
142
|
+
: route.name;
|
|
143
|
+
const rawText =
|
|
144
|
+
h1Text || document.title || routeName || globalThis.location.pathname;
|
|
145
|
+
|
|
146
|
+
return `${prefix}${rawText}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function manageFocus(h1: HTMLElement | null): void {
|
|
150
|
+
if (!h1) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!h1.hasAttribute("tabindex")) {
|
|
155
|
+
h1.setAttribute("tabindex", "-1");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
h1.focus({ preventScroll: true });
|
|
159
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { injectRouter } from "./injectRouter";
|
|
2
|
+
|
|
3
|
+
export { injectNavigator } from "./injectNavigator";
|
|
4
|
+
|
|
5
|
+
export { injectRoute } from "./injectRoute";
|
|
6
|
+
|
|
7
|
+
export { injectRouteNode } from "./injectRouteNode";
|
|
8
|
+
|
|
9
|
+
export { injectRouteUtils } from "./injectRouteUtils";
|
|
10
|
+
|
|
11
|
+
export { injectRouterTransition } from "./injectRouterTransition";
|
|
12
|
+
|
|
13
|
+
export { injectIsActiveRoute } from "./injectIsActiveRoute";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createActiveRouteSource } from "@real-router/sources";
|
|
2
|
+
|
|
3
|
+
import { sourceToSignal } from "../sourceToSignal";
|
|
4
|
+
import { injectRouter } from "./injectRouter";
|
|
5
|
+
|
|
6
|
+
import type { Signal } from "@angular/core";
|
|
7
|
+
import type { Params } from "@real-router/core";
|
|
8
|
+
|
|
9
|
+
export function injectIsActiveRoute(
|
|
10
|
+
routeName: string,
|
|
11
|
+
params?: Params,
|
|
12
|
+
options?: { strict?: boolean; ignoreQueryParams?: boolean },
|
|
13
|
+
): Signal<boolean> {
|
|
14
|
+
const router = injectRouter();
|
|
15
|
+
const source = createActiveRouteSource(router, routeName, params, {
|
|
16
|
+
strict: options?.strict ?? false,
|
|
17
|
+
ignoreQueryParams: options?.ignoreQueryParams ?? true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return sourceToSignal(source);
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { inject } from "@angular/core";
|
|
2
|
+
|
|
3
|
+
import type { InjectionToken } from "@angular/core";
|
|
4
|
+
|
|
5
|
+
export function injectOrThrow<T>(token: InjectionToken<T>, fnName: string): T {
|
|
6
|
+
const value = inject(token, { optional: true });
|
|
7
|
+
|
|
8
|
+
if (!value) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`${fnName} must be used within a provideRealRouter context`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getNavigator } from "@real-router/core";
|
|
2
|
+
import { createRouteNodeSource } from "@real-router/sources";
|
|
3
|
+
|
|
4
|
+
import { sourceToSignal } from "../sourceToSignal";
|
|
5
|
+
import { injectRouter } from "./injectRouter";
|
|
6
|
+
|
|
7
|
+
import type { RouteSignals } from "../types";
|
|
8
|
+
|
|
9
|
+
export function injectRouteNode(nodeName: string): RouteSignals {
|
|
10
|
+
const router = injectRouter();
|
|
11
|
+
const navigator = getNavigator(router);
|
|
12
|
+
const source = createRouteNodeSource(router, nodeName);
|
|
13
|
+
const routeState = sourceToSignal(source);
|
|
14
|
+
|
|
15
|
+
return { routeState, navigator };
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getPluginApi } from "@real-router/core/api";
|
|
2
|
+
import { getRouteUtils } from "@real-router/route-utils";
|
|
3
|
+
|
|
4
|
+
import { injectRouter } from "./injectRouter";
|
|
5
|
+
|
|
6
|
+
import type { RouteUtils } from "@real-router/route-utils";
|
|
7
|
+
|
|
8
|
+
export function injectRouteUtils(): RouteUtils {
|
|
9
|
+
const router = injectRouter();
|
|
10
|
+
|
|
11
|
+
return getRouteUtils(getPluginApi(router).getTree());
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createTransitionSource } from "@real-router/sources";
|
|
2
|
+
|
|
3
|
+
import { sourceToSignal } from "../sourceToSignal";
|
|
4
|
+
import { injectRouter } from "./injectRouter";
|
|
5
|
+
|
|
6
|
+
import type { Signal } from "@angular/core";
|
|
7
|
+
import type { RouterTransitionSnapshot } from "@real-router/sources";
|
|
8
|
+
|
|
9
|
+
export function injectRouterTransition(): Signal<RouterTransitionSnapshot> {
|
|
10
|
+
const router = injectRouter();
|
|
11
|
+
const source = createTransitionSource(router);
|
|
12
|
+
|
|
13
|
+
return sourceToSignal(source);
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export { provideRealRouter, ROUTER, NAVIGATOR, ROUTE } from "./providers";
|
|
2
|
+
|
|
3
|
+
export { sourceToSignal } from "./sourceToSignal";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
injectRouter,
|
|
7
|
+
injectNavigator,
|
|
8
|
+
injectRoute,
|
|
9
|
+
injectRouteNode,
|
|
10
|
+
injectRouteUtils,
|
|
11
|
+
injectRouterTransition,
|
|
12
|
+
injectIsActiveRoute,
|
|
13
|
+
} from "./functions";
|
|
14
|
+
|
|
15
|
+
export { RouteView } from "./components/RouteView";
|
|
16
|
+
|
|
17
|
+
export { RouterErrorBoundary } from "./components/RouterErrorBoundary";
|
|
18
|
+
|
|
19
|
+
export type { ErrorContext } from "./components/RouterErrorBoundary";
|
|
20
|
+
|
|
21
|
+
export { NavigationAnnouncer } from "./components/NavigationAnnouncer";
|
|
22
|
+
|
|
23
|
+
export { RouteMatch } from "./directives/RouteMatch";
|
|
24
|
+
|
|
25
|
+
export { RouteNotFound } from "./directives/RouteNotFound";
|
|
26
|
+
|
|
27
|
+
export { RealLink } from "./directives/RealLink";
|
|
28
|
+
|
|
29
|
+
export { RealLinkActive } from "./directives/RealLinkActive";
|
|
30
|
+
|
|
31
|
+
export type { RouteSignals } from "./types";
|
|
32
|
+
|
|
33
|
+
export type {
|
|
34
|
+
RouteSnapshot,
|
|
35
|
+
RouterTransitionSnapshot,
|
|
36
|
+
RouterErrorSnapshot,
|
|
37
|
+
} from "@real-router/sources";
|
|
38
|
+
|
|
39
|
+
export type { Navigator } from "@real-router/core";
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InjectionToken,
|
|
3
|
+
makeEnvironmentProviders,
|
|
4
|
+
type EnvironmentProviders,
|
|
5
|
+
} from "@angular/core";
|
|
6
|
+
import { getNavigator, type Router, type Navigator } from "@real-router/core";
|
|
7
|
+
import { createRouteSource } from "@real-router/sources";
|
|
8
|
+
|
|
9
|
+
import { sourceToSignal } from "./sourceToSignal";
|
|
10
|
+
|
|
11
|
+
import type { RouteSignals } from "./types";
|
|
12
|
+
|
|
13
|
+
export const ROUTER = new InjectionToken<Router>("ROUTER");
|
|
14
|
+
|
|
15
|
+
export const NAVIGATOR = new InjectionToken<Navigator>("NAVIGATOR");
|
|
16
|
+
|
|
17
|
+
export const ROUTE = new InjectionToken<RouteSignals>("ROUTE");
|
|
18
|
+
|
|
19
|
+
export function provideRealRouter(router: Router): EnvironmentProviders {
|
|
20
|
+
const navigator = getNavigator(router);
|
|
21
|
+
|
|
22
|
+
return makeEnvironmentProviders([
|
|
23
|
+
{ provide: ROUTER, useValue: router },
|
|
24
|
+
{ provide: NAVIGATOR, useValue: navigator },
|
|
25
|
+
{
|
|
26
|
+
provide: ROUTE,
|
|
27
|
+
useFactory: (): RouteSignals => ({
|
|
28
|
+
routeState: sourceToSignal(createRouteSource(router)),
|
|
29
|
+
navigator,
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { signal, type Signal, inject, DestroyRef } from "@angular/core";
|
|
2
|
+
|
|
3
|
+
import type { RouterSource } from "@real-router/sources";
|
|
4
|
+
|
|
5
|
+
/** Must be called within an injection context (constructor, field initializer, runInInjectionContext). */
|
|
6
|
+
export function sourceToSignal<T>(source: RouterSource<T>): Signal<T> {
|
|
7
|
+
const sig = signal<T>(source.getSnapshot());
|
|
8
|
+
const destroyRef = inject(DestroyRef);
|
|
9
|
+
|
|
10
|
+
const unsubscribe = source.subscribe(() => {
|
|
11
|
+
sig.set(source.getSnapshot());
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
destroyRef.onDestroy(() => {
|
|
15
|
+
unsubscribe();
|
|
16
|
+
source.destroy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return sig.asReadonly();
|
|
20
|
+
}
|