@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.
- package/README.md +183 -4
- package/dist/README.md +183 -4
- package/dist/fesm2022/real-router-angular-ssr.mjs +323 -0
- package/dist/fesm2022/real-router-angular-ssr.mjs.map +1 -0
- package/dist/fesm2022/real-router-angular.mjs +759 -173
- package/dist/fesm2022/real-router-angular.mjs.map +1 -1
- package/dist/types/real-router-angular-ssr.d.ts +227 -0
- package/dist/types/real-router-angular-ssr.d.ts.map +1 -0
- package/dist/types/real-router-angular.d.ts +119 -20
- package/dist/types/real-router-angular.d.ts.map +1 -1
- package/package.json +18 -11
- package/src/components/RouteView.ts +81 -56
- package/src/components/RouterErrorBoundary.ts +7 -5
- package/src/directives/RealLink.ts +57 -37
- package/src/directives/RealLinkActive.ts +34 -25
- package/src/dom-utils/link-utils.ts +119 -7
- package/src/dom-utils/route-announcer.ts +58 -2
- package/src/dom-utils/scroll-restore.ts +160 -12
- package/src/functions/injectIsActiveRoute.ts +9 -8
- package/src/functions/injectNavigator.ts +4 -0
- package/src/functions/injectOrThrow.ts +5 -1
- package/src/functions/injectRoute.ts +17 -8
- package/src/functions/injectRouteEnter.ts +5 -10
- package/src/functions/injectRouteNode.ts +3 -0
- package/src/functions/injectRouteUtils.ts +3 -0
- package/src/functions/injectRouter.ts +4 -0
- package/src/functions/injectRouterTransition.ts +3 -0
- package/src/index.ts +14 -3
- package/src/internal/buildActiveRouteOptions.ts +20 -0
- package/src/internal/install.ts +77 -0
- package/src/internal/subscribeSourceToSignal.ts +48 -0
- package/src/providers.ts +11 -38
- package/src/providersFactory.ts +298 -0
- package/src/sourceToSignal.ts +10 -2
- package/src/types.ts +6 -1
- package/ssr/components/ClientOnly.ts +27 -0
- package/ssr/components/HttpStatusCode.ts +106 -0
- package/ssr/components/ServerOnly.ts +27 -0
- package/ssr/functions/injectDeferred.ts +92 -0
- package/ssr/functions/provideHttpStatusSink.ts +43 -0
- package/ssr/ng-package.json +6 -0
- package/ssr/public_api.ts +35 -0
- package/ssr/utils/createHttpStatusSink.ts +61 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { inject, DestroyRef, ApplicationRef, signal, InjectionToken, provideEnvironmentInitializer, makeEnvironmentProviders, makeStateKey, REQUEST, provideAppInitializer, TransferState, assertInInjectionContext, effect, input, TemplateRef, Directive, contentChildren, computed, Component, output, ElementRef } from '@angular/core';
|
|
3
3
|
import { getNavigator, UNKNOWN_ROUTE } from '@real-router/core';
|
|
4
4
|
import { createRouteSource, createRouteNodeSource, getTransitionSource, createActiveRouteSource, createDismissableError } from '@real-router/sources';
|
|
5
|
-
import { getPluginApi } from '@real-router/core/api';
|
|
5
|
+
import { cloneRouter, getPluginApi } from '@real-router/core/api';
|
|
6
|
+
import { hydrateRouter, serializeRouterState } from '@real-router/core/utils';
|
|
6
7
|
import { getRouteUtils, startsWithSegment } from '@real-router/route-utils';
|
|
7
8
|
import { NgTemplateOutlet } from '@angular/common';
|
|
8
9
|
|
|
9
|
-
const NOOP_INSTANCE$
|
|
10
|
+
const NOOP_INSTANCE$3 = Object.freeze({
|
|
10
11
|
destroy: () => {
|
|
11
12
|
/* no-op */
|
|
12
13
|
},
|
|
@@ -34,7 +35,7 @@ const NOOP_INSTANCE$2 = Object.freeze({
|
|
|
34
35
|
*/
|
|
35
36
|
function createDirectionTracker(router) {
|
|
36
37
|
if (typeof document === "undefined") {
|
|
37
|
-
return NOOP_INSTANCE$
|
|
38
|
+
return NOOP_INSTANCE$3;
|
|
38
39
|
}
|
|
39
40
|
let popstateFlag = false;
|
|
40
41
|
document.documentElement.dataset.navDirection = "forward";
|
|
@@ -69,7 +70,24 @@ const SAFARI_READY_DELAY = 100;
|
|
|
69
70
|
const ANNOUNCER_ATTR = "data-real-router-announcer";
|
|
70
71
|
const INTERNAL_ROUTE_PREFIX = "@@";
|
|
71
72
|
const VISUALLY_HIDDEN = "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";
|
|
73
|
+
const NOOP_INSTANCE$2 = Object.freeze({
|
|
74
|
+
destroy: () => {
|
|
75
|
+
/* no-op */
|
|
76
|
+
},
|
|
77
|
+
});
|
|
72
78
|
function createRouteAnnouncer(router, options) {
|
|
79
|
+
// Defensive SSR / non-browser guard: in SSR (Node.js) or non-DOM
|
|
80
|
+
// environments, `document` is undefined and the announcer cannot
|
|
81
|
+
// attach its aria-live region. Return a frozen NOOP_INSTANCE — same
|
|
82
|
+
// pattern as `createDirectionTracker`, `createScrollRestoration`, and
|
|
83
|
+
// `createViewTransitions`. Without this guard, `NavigationAnnouncer`
|
|
84
|
+
// component construction would throw `ReferenceError: document is not
|
|
85
|
+
// defined` under `@angular/ssr` rendering, tearing down the whole SSR
|
|
86
|
+
// bootstrap. Closes review-2026-05-10 §5.10 ⛔ "NavigationAnnouncer
|
|
87
|
+
// SSR mode" MED.
|
|
88
|
+
if (typeof document === "undefined") {
|
|
89
|
+
return NOOP_INSTANCE$2;
|
|
90
|
+
}
|
|
73
91
|
const prefix = options?.prefix ?? "Navigated to ";
|
|
74
92
|
const getCustomText = options?.getAnnouncementText;
|
|
75
93
|
let isInitialNavigation = true;
|
|
@@ -150,7 +168,19 @@ function getOrCreateAnnouncer() {
|
|
|
150
168
|
element.setAttribute("aria-live", "assertive");
|
|
151
169
|
element.setAttribute("aria-atomic", "true");
|
|
152
170
|
element.setAttribute(ANNOUNCER_ATTR, "");
|
|
153
|
-
|
|
171
|
+
// Defensive SSR / pre-`<body>` guard: in some environments (early
|
|
172
|
+
// injection, deferred-body documents, certain SSR rehydration paths)
|
|
173
|
+
// `document.body` can be null when the announcer is constructed.
|
|
174
|
+
// `document.body.prepend(...)` would throw `TypeError: Cannot read
|
|
175
|
+
// properties of null`, tearing down the consumer's RouterProvider /
|
|
176
|
+
// NavigationAnnouncer mount. Fallback to `documentElement` keeps the
|
|
177
|
+
// announcer working for SR users; visual-hidden styling means there is
|
|
178
|
+
// no visible artifact regardless of mount point.
|
|
179
|
+
//
|
|
180
|
+
// TS dom lib types `document.body` as `HTMLElement` (non-null), but
|
|
181
|
+
// runtime can return null per spec. The `as` cast narrows the type to
|
|
182
|
+
// include null so the `??` short-circuit is type-safe.
|
|
183
|
+
(document.body ?? document.documentElement).prepend(element);
|
|
154
184
|
return element;
|
|
155
185
|
}
|
|
156
186
|
function removeAnnouncer() {
|
|
@@ -158,7 +188,27 @@ function removeAnnouncer() {
|
|
|
158
188
|
}
|
|
159
189
|
function resolveText(route, prefix, getCustomText, h1) {
|
|
160
190
|
if (getCustomText) {
|
|
161
|
-
|
|
191
|
+
try {
|
|
192
|
+
const customText = getCustomText(route);
|
|
193
|
+
// Mini-sprint E.4 (audit-5 §4.2 #4) — empty-string fallback.
|
|
194
|
+
// A consumer pattern like
|
|
195
|
+
// getAnnouncementText: (route) => myMap[route.name] ?? ""
|
|
196
|
+
// returns `""` for routes outside the map. The subscribe loop
|
|
197
|
+
// then sees an empty text and silently no-announces — screen
|
|
198
|
+
// readers stay quiet without any signal to the developer. Treat
|
|
199
|
+
// a falsy custom result (`""` / `null` / `undefined`) as
|
|
200
|
+
// "consumer doesn't have a name for this route" and fall through
|
|
201
|
+
// to the default resolution chain (h1 → title → route name).
|
|
202
|
+
if (customText) {
|
|
203
|
+
return customText;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// A throwing consumer callback inside the router's subscribe loop
|
|
208
|
+
// would tear down sibling listeners — log and fall through to the
|
|
209
|
+
// built-in resolution chain so the announcer keeps working.
|
|
210
|
+
console.error("[real-router] getAnnouncementText threw; falling back to default resolution.", error);
|
|
211
|
+
}
|
|
162
212
|
}
|
|
163
213
|
const h1Text = (h1?.textContent ?? "").trim();
|
|
164
214
|
const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)
|
|
@@ -201,20 +251,36 @@ function createScrollRestoration(router, options) {
|
|
|
201
251
|
const getContainer = options?.scrollContainer;
|
|
202
252
|
const behavior = options?.behavior ?? "auto";
|
|
203
253
|
const storageKey = options?.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
254
|
+
// Write-through in-memory cache: parse sessionStorage once per provider
|
|
255
|
+
// mount, then mutate in-memory. Avoids a JSON.parse + JSON.stringify pair
|
|
256
|
+
// on every subscribeLeave / pagehide event.
|
|
257
|
+
let store;
|
|
204
258
|
const loadStore = () => {
|
|
259
|
+
if (store !== undefined) {
|
|
260
|
+
return store;
|
|
261
|
+
}
|
|
205
262
|
try {
|
|
206
263
|
const raw = sessionStorage.getItem(storageKey);
|
|
207
|
-
|
|
264
|
+
store = raw ? JSON.parse(raw) : {};
|
|
208
265
|
}
|
|
209
266
|
catch {
|
|
210
|
-
|
|
267
|
+
store = {};
|
|
211
268
|
}
|
|
269
|
+
return store;
|
|
212
270
|
};
|
|
213
271
|
const putPos = (key, pos) => {
|
|
214
272
|
try {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
273
|
+
const cached = loadStore();
|
|
274
|
+
// Skip-same-value: when a route is left at the same scroll position it
|
|
275
|
+
// already holds in the cache (e.g. tab-switching without scrolling),
|
|
276
|
+
// both the in-memory write and the JSON.stringify + setItem pair are
|
|
277
|
+
// no-ops. Eliminates redundant serialization on the navigation hot
|
|
278
|
+
// path for the common "click tabs without scrolling" case.
|
|
279
|
+
if (cached[key] === pos) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
cached[key] = pos;
|
|
283
|
+
sessionStorage.setItem(storageKey, JSON.stringify(cached));
|
|
218
284
|
}
|
|
219
285
|
catch {
|
|
220
286
|
// Ignore quota / security errors.
|
|
@@ -285,6 +351,27 @@ function createScrollRestoration(router, options) {
|
|
|
285
351
|
writePos(0);
|
|
286
352
|
};
|
|
287
353
|
let destroyed = false;
|
|
354
|
+
let unserializableWarned = false;
|
|
355
|
+
// `keyOf` defers to `canonicalJson` which calls `JSON.stringify`. Two
|
|
356
|
+
// realistic inputs blow up the serializer and would otherwise crash the
|
|
357
|
+
// subscribe callback (taking scroll-restore offline for the whole session):
|
|
358
|
+
// - `BigInt` params → `TypeError: Do not know how to serialize a BigInt`
|
|
359
|
+
// - cyclic params (reactive proxies, DOM-ref back-pointers) → stack
|
|
360
|
+
// overflow.
|
|
361
|
+
// The defensive wrapper drops capture/restore for that specific navigation
|
|
362
|
+
// and warns once per provider — the rest of the cache stays usable.
|
|
363
|
+
const safeKeyOf = (state) => {
|
|
364
|
+
try {
|
|
365
|
+
return keyOf(state);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
if (!unserializableWarned) {
|
|
369
|
+
unserializableWarned = true;
|
|
370
|
+
console.error(`[real-router] scroll-restore: route "${state.name}" has params that cannot be canonicalized (e.g. BigInt or cyclic structure). Scroll position will not be captured or restored for this route.`);
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
288
375
|
const unsubscribe = router.subscribe(({ route, previousRoute }) => {
|
|
289
376
|
const nav = route.context
|
|
290
377
|
.navigation;
|
|
@@ -292,7 +379,10 @@ function createScrollRestoration(router, options) {
|
|
|
292
379
|
// previousRoute is undefined and capture is naturally skipped. The
|
|
293
380
|
// pre-refresh position was already persisted via pagehide.
|
|
294
381
|
if (previousRoute) {
|
|
295
|
-
|
|
382
|
+
const prevKey = safeKeyOf(previousRoute);
|
|
383
|
+
if (prevKey !== null) {
|
|
384
|
+
putPos(prevKey, readPos());
|
|
385
|
+
}
|
|
296
386
|
}
|
|
297
387
|
// Single rAF so DOM is committed before we read anchors / write scroll.
|
|
298
388
|
// Guard against destroy() racing with the callback.
|
|
@@ -310,7 +400,8 @@ function createScrollRestoration(router, options) {
|
|
|
310
400
|
if (nav.direction === "back" ||
|
|
311
401
|
nav.navigationType === "traverse" ||
|
|
312
402
|
nav.navigationType === "reload") {
|
|
313
|
-
|
|
403
|
+
const key = safeKeyOf(route);
|
|
404
|
+
writePos(key === null ? 0 : (loadStore()[key] ?? 0));
|
|
314
405
|
return;
|
|
315
406
|
}
|
|
316
407
|
scrollToHashOrTop(route);
|
|
@@ -319,7 +410,10 @@ function createScrollRestoration(router, options) {
|
|
|
319
410
|
const onPageHide = () => {
|
|
320
411
|
const current = router.getState();
|
|
321
412
|
if (current) {
|
|
322
|
-
|
|
413
|
+
const key = safeKeyOf(current);
|
|
414
|
+
if (key !== null) {
|
|
415
|
+
putPos(key, readPos());
|
|
416
|
+
}
|
|
323
417
|
}
|
|
324
418
|
};
|
|
325
419
|
globalThis.addEventListener("pagehide", onPageHide);
|
|
@@ -340,15 +434,103 @@ function createScrollRestoration(router, options) {
|
|
|
340
434
|
},
|
|
341
435
|
};
|
|
342
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Internal cache-key builder for scroll-position storage.
|
|
439
|
+
*
|
|
440
|
+
* **Exported for testing only — not part of the public API** (intentionally
|
|
441
|
+
* excluded from `index.ts` barrel). Adapter property tests import it via
|
|
442
|
+
* the direct path to lock the `(name, canonicalJson(params))` key shape
|
|
443
|
+
* as a regression guard (§8b H20 / audit-2026-05-16 #S3). A change to
|
|
444
|
+
* key format would silently lose scroll positions across an upgrade —
|
|
445
|
+
* the test set is the contract.
|
|
446
|
+
*
|
|
447
|
+
* ## Identity-based memoization (audit-2026-05-17 §8b #2)
|
|
448
|
+
*
|
|
449
|
+
* `State` objects emitted by core are frozen per-navigation: their
|
|
450
|
+
* `name` / `params` are immutable for the lifetime of the snapshot, and
|
|
451
|
+
* any change produces a new `State` reference. A `WeakMap<State, string>`
|
|
452
|
+
* therefore safely caches the canonicalised key by identity — repeat
|
|
453
|
+
* `keyOf(state)` calls on the same snapshot (typical on
|
|
454
|
+
* back/forward/traverse where the same prior `State` is re-emitted)
|
|
455
|
+
* skip the recursive `canonicalJson` pass entirely.
|
|
456
|
+
*
|
|
457
|
+
* The cache key is the `State` reference, so entries auto-release when
|
|
458
|
+
* the snapshot is GC'd — no eviction needed.
|
|
459
|
+
*/
|
|
460
|
+
const KEY_CACHE = new WeakMap();
|
|
343
461
|
function keyOf(state) {
|
|
344
|
-
|
|
462
|
+
const cached = KEY_CACHE.get(state);
|
|
463
|
+
if (cached !== undefined) {
|
|
464
|
+
return cached;
|
|
465
|
+
}
|
|
466
|
+
const key = `${state.name}:${canonicalJson(state.params)}`;
|
|
467
|
+
KEY_CACHE.set(state, key);
|
|
468
|
+
return key;
|
|
345
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Stable JSON serializer with sorted object keys.
|
|
472
|
+
*
|
|
473
|
+
* **Exported for testing only — not part of the public API** (intentionally
|
|
474
|
+
* excluded from `index.ts` barrel). Adapter property tests import it via
|
|
475
|
+
* the direct path to lock the key-order-insensitive property
|
|
476
|
+
* (`canonicalJson({a:1,b:2}) === canonicalJson({b:2,a:1})`).
|
|
477
|
+
*
|
|
478
|
+
* ## Divergence from `@real-router/sources/canonicalJson` — by design
|
|
479
|
+
*
|
|
480
|
+
* Two independent implementations live in the monorepo:
|
|
481
|
+
*
|
|
482
|
+
* - **`shared/dom-utils/scroll-restore.canonicalJson`** (this file) — scroll
|
|
483
|
+
* cache key builder. Uses `localeCompare` and a plain-object accumulator;
|
|
484
|
+
* tolerates `__proto__`-keyed inputs only insofar as `JSON.stringify`'s
|
|
485
|
+
* replacer happens to sort them; relies on `JSON.stringify`'s native cycle
|
|
486
|
+
* detector. Designed to be cheap on the navigation hot path. The
|
|
487
|
+
* surrounding [[safeKeyOf]] wrapper catches the two crash inputs (`BigInt`,
|
|
488
|
+
* cyclic) and skips the offending capture/restore.
|
|
489
|
+
*
|
|
490
|
+
* - **`@real-router/sources/canonicalJson`** — sources cache key builder.
|
|
491
|
+
* Uses byte-order compare (`< / >`) for locale-independence, a
|
|
492
|
+
* `Object.create(null)` accumulator to prevent prototype pollution, and a
|
|
493
|
+
* bespoke path-based cycle detector (the native one cannot see the cloned
|
|
494
|
+
* graph). Throws eagerly on `Map`/`Set`/`RegExp`/cycles — the caller falls
|
|
495
|
+
* back to a non-cached source.
|
|
496
|
+
*
|
|
497
|
+
* **They are intentionally NOT interchangeable.** Aligning them would either
|
|
498
|
+
* regress scroll-restore performance (byte-order + recursive clone is heavier
|
|
499
|
+
* per call) or weaken the sources cache (locale dependence breaks
|
|
500
|
+
* deterministic cache keys across machines). No cross-package equivalence
|
|
501
|
+
* test exists or should be added; the relationship is "different invariants,
|
|
502
|
+
* different costs, different consumers." Audit-2 / audit-2026-05-17 §2
|
|
503
|
+
* documents the choice.
|
|
504
|
+
*/
|
|
346
505
|
function canonicalJson(value) {
|
|
347
506
|
return JSON.stringify(value, canonicalReplacer);
|
|
348
507
|
}
|
|
349
508
|
function canonicalReplacer(_key, val) {
|
|
509
|
+
// audit-2026-05-17 §5 MEDIUM (Sprint A.3) — function/Symbol marker.
|
|
510
|
+
// `JSON.stringify` silently drops function and symbol values from
|
|
511
|
+
// object output. Two routes that differ ONLY in a function/Symbol
|
|
512
|
+
// value would canonicalize to the same string → silent scroll-cache
|
|
513
|
+
// key collision (positions clobber each other). Replacing the value
|
|
514
|
+
// with a sentinel string breaks the collision while keeping the
|
|
515
|
+
// canonical form deterministic. The sentinels are intentionally
|
|
516
|
+
// ASCII-only and lexically distinct from valid JSON-stringified
|
|
517
|
+
// values; consumers will see `"<fn>"` / `"<sym>"` if they ever
|
|
518
|
+
// round-trip the cache key, signalling the substitution clearly.
|
|
519
|
+
if (typeof val === "function") {
|
|
520
|
+
return "<fn>";
|
|
521
|
+
}
|
|
522
|
+
if (typeof val === "symbol") {
|
|
523
|
+
return "<sym>";
|
|
524
|
+
}
|
|
350
525
|
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
351
|
-
|
|
526
|
+
// Null-prototype accumulator: a plain `{}` would interpret
|
|
527
|
+
// `sorted["__proto__"] = x` as a prototype assignment (silently dropped
|
|
528
|
+
// from JSON.stringify output AND a prototype-pollution vector). Mirrors
|
|
529
|
+
// the same guard in `@real-router/sources/canonicalJson`. The two
|
|
530
|
+
// implementations are still intentionally divergent (see the doc-block
|
|
531
|
+
// on [[canonicalJson]] above), but prototype-safety is non-negotiable
|
|
532
|
+
// on both. Lock-test: scrollRestoreKey.properties.ts Invariant 11.
|
|
533
|
+
const sorted = Object.create(null);
|
|
352
534
|
// eslint-disable-next-line unicorn/no-array-sort -- ng-packagr uses pre-ES2023 lib; toSorted unavailable
|
|
353
535
|
const keys = Object.keys(val).sort((left, right) => left.localeCompare(right));
|
|
354
536
|
for (const key of keys) {
|
|
@@ -485,14 +667,39 @@ function shouldNavigate(evt) {
|
|
|
485
667
|
!evt.ctrlKey &&
|
|
486
668
|
!evt.shiftKey);
|
|
487
669
|
}
|
|
670
|
+
// Matches a single percent-escape triple (`%` + two hex digits). Used as
|
|
671
|
+
// the "already-encoded" probe in `encodeFragmentInline` below — see the
|
|
672
|
+
// idempotency rationale there.
|
|
673
|
+
const PERCENT_ESCAPE_PROBE = /%[\dA-Fa-f]{2}/;
|
|
488
674
|
/**
|
|
489
675
|
* RFC 3986 fragment encoding: preserve sub-delims (`&`, `=`, `?`, `:`),
|
|
490
676
|
* encode space, `%`, control chars, non-ASCII via encodeURI; defensively
|
|
491
677
|
* escape `#` (encodeURI does not). Mirrors `encodeHashFragment` in
|
|
492
678
|
* `shared/browser-env/url-context.ts` — duplicated here because the
|
|
493
679
|
* shared/dom-utils symlink graph does not reach shared/browser-env.
|
|
680
|
+
*
|
|
681
|
+
* **Idempotency for pre-encoded input (audit-2026-05-17 §5 MEDIUM E.1).**
|
|
682
|
+
* The doc-comment on `<Link hash>` says the value is a "decoded fragment
|
|
683
|
+
* without leading #". But realistic consumers copy hashes out of
|
|
684
|
+
* `location.hash` (which is percent-encoded) and pass them back, so the
|
|
685
|
+
* naive `encodeURI("%20")` would double-encode into `"%2520"` and break
|
|
686
|
+
* anchor lookup. We detect a percent-escape triple in the input and, if
|
|
687
|
+
* present, decode + re-encode for idempotency. Malformed `%XX` (e.g.
|
|
688
|
+
* `"%2"` or `"%ZZ"`) makes `decodeURIComponent` throw — in that case we
|
|
689
|
+
* fall through to plain `encodeURI`, which never throws.
|
|
494
690
|
*/
|
|
495
691
|
function encodeFragmentInline(decoded) {
|
|
692
|
+
if (PERCENT_ESCAPE_PROBE.test(decoded)) {
|
|
693
|
+
try {
|
|
694
|
+
const roundtrip = decodeURIComponent(decoded);
|
|
695
|
+
return encodeURI(roundtrip).replaceAll("#", "%23");
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
// Malformed `%XX` — fall through to the plain encoding path.
|
|
699
|
+
// encodeURI does not throw on malformed escapes; it treats the
|
|
700
|
+
// `%` as a literal and percent-encodes it (`%2` → `%252`).
|
|
701
|
+
}
|
|
702
|
+
}
|
|
496
703
|
return encodeURI(decoded).replaceAll("#", "%23");
|
|
497
704
|
}
|
|
498
705
|
/**
|
|
@@ -518,11 +725,28 @@ function buildHref(router, routeName, routeParams, options) {
|
|
|
518
725
|
const buildUrl = router.buildUrl;
|
|
519
726
|
if (buildUrl) {
|
|
520
727
|
const url = buildUrl(routeName, routeParams, normHash === undefined ? undefined : { hash: normHash });
|
|
521
|
-
|
|
728
|
+
// Accept only non-empty strings. The BuildUrlFn type contract is
|
|
729
|
+
// `string | undefined`, but defensive against:
|
|
730
|
+
// - `""` (empty string) → would render `<a href="">`, which resolves
|
|
731
|
+
// to the current page URL → silent self-navigation on click.
|
|
732
|
+
// - `null` (type-contract violation) → would render `<a href={null}>`,
|
|
733
|
+
// stringified to `"null"` in some renderers.
|
|
734
|
+
// Either case falls through to the `router.buildPath` fallback below.
|
|
735
|
+
if (typeof url === "string" && url.length > 0) {
|
|
522
736
|
return url;
|
|
523
737
|
}
|
|
524
738
|
}
|
|
525
739
|
const path = router.buildPath(routeName, routeParams);
|
|
740
|
+
// Symmetric to the buildUrl guard above (#S1 audit, Invariant 12).
|
|
741
|
+
// `router.buildPath` is typed `string`, but defends against:
|
|
742
|
+
// - `""` (empty string) — would render `<a href="">`, which resolves
|
|
743
|
+
// to the current page URL → silent self-navigation on click.
|
|
744
|
+
// - non-string type-contract violations from custom path-matchers.
|
|
745
|
+
// Both yield `undefined` (renderer drops the attribute) with a warning.
|
|
746
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
747
|
+
console.error(`[real-router] Route "${routeName}" yielded an empty path. The element will render without an href attribute.`);
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
526
750
|
return normHash ? `${path}#${encodeFragmentInline(normHash)}` : path;
|
|
527
751
|
}
|
|
528
752
|
catch {
|
|
@@ -548,8 +772,25 @@ function navigateWithHash(router, routeName, routeParams, hash, extraOptions) {
|
|
|
548
772
|
}
|
|
549
773
|
return router.navigate(routeName, routeParams, opts);
|
|
550
774
|
}
|
|
775
|
+
// Match-any-whitespace regex shared across calls. RegExp literals at
|
|
776
|
+
// call-site recompile in some engines; lifting it avoids that microcost
|
|
777
|
+
// for the slow-path branch.
|
|
778
|
+
const WHITESPACE_PROBE = /\s/;
|
|
779
|
+
const WHITESPACE_SPLIT = /\S+/g;
|
|
551
780
|
function parseTokens(value) {
|
|
552
|
-
|
|
781
|
+
if (!value) {
|
|
782
|
+
return [];
|
|
783
|
+
}
|
|
784
|
+
// Hot-path fast-path (audit-2026-05-17 §8b #1): >99% of active-class
|
|
785
|
+
// inputs at `<Link>` emit are single-token strings like `"active"` or
|
|
786
|
+
// `"is-current"` — no whitespace, no leading/trailing pad. Skip the
|
|
787
|
+
// regex match and Array result allocation: a literal `[value]` works
|
|
788
|
+
// because the slow-path `match(/\S+/g)` would return exactly `[value]`
|
|
789
|
+
// for the same input. PBT lock: linkUtils.properties.ts Invariant 13.
|
|
790
|
+
if (!WHITESPACE_PROBE.test(value)) {
|
|
791
|
+
return [value];
|
|
792
|
+
}
|
|
793
|
+
return value.match(WHITESPACE_SPLIT) ?? [];
|
|
553
794
|
}
|
|
554
795
|
function buildActiveClassName(isActive, activeClassName, baseClassName) {
|
|
555
796
|
if (isActive && activeClassName) {
|
|
@@ -572,6 +813,29 @@ function buildActiveClassName(isActive, activeClassName, baseClassName) {
|
|
|
572
813
|
}
|
|
573
814
|
return baseClassName ?? undefined;
|
|
574
815
|
}
|
|
816
|
+
/**
|
|
817
|
+
* One-level structural equality using `Object.is` per key.
|
|
818
|
+
*
|
|
819
|
+
* **String-keyed properties only (Mini-sprint E.3 — audit-5 §4.2 #3).**
|
|
820
|
+
* Implementation walks `Object.keys()` which by spec returns only
|
|
821
|
+
* enumerable own STRING keys. Symbol-keyed properties — created via
|
|
822
|
+
* `obj[Symbol("brand")] = value` or `{ [Symbol(...)]: value }` — are
|
|
823
|
+
* NOT compared. Two records that differ only in a Symbol-keyed value
|
|
824
|
+
* will compare as equal.
|
|
825
|
+
*
|
|
826
|
+
* This is intentional: route params and Link options are documented as
|
|
827
|
+
* string-keyed primitives (string | number | boolean) — Symbol-keyed
|
|
828
|
+
* metadata (e.g. brand markers, private state) doesn't belong in a
|
|
829
|
+
* cache-key comparison. Switching to `Reflect.ownKeys()` would extend
|
|
830
|
+
* the contract to symbols at the cost of one extra allocation per call
|
|
831
|
+
* (Reflect.ownKeys composes string-keys + symbol-keys arrays). If a
|
|
832
|
+
* consumer relies on symbol-keyed metadata for navigation
|
|
833
|
+
* disambiguation, they should encode it into a string key instead.
|
|
834
|
+
*
|
|
835
|
+
* Mirrors React's `shallowEqual` (packages/shared/shallowEqual.js) in
|
|
836
|
+
* both the string-keys-only semantics and the `hasOwnProperty` guard
|
|
837
|
+
* below.
|
|
838
|
+
*/
|
|
575
839
|
function shallowEqual(prev, next) {
|
|
576
840
|
if (Object.is(prev, next)) {
|
|
577
841
|
return true;
|
|
@@ -586,7 +850,11 @@ function shallowEqual(prev, next) {
|
|
|
586
850
|
const prevRecord = prev;
|
|
587
851
|
const nextRecord = next;
|
|
588
852
|
for (const key of prevKeys) {
|
|
589
|
-
|
|
853
|
+
// hasOwnProperty guard: without it, a key missing in `next` reads as
|
|
854
|
+
// `undefined` and falsely matches `prev[key] === undefined`. Same shape
|
|
855
|
+
// as React's shallowEqual (packages/shared/shallowEqual.js).
|
|
856
|
+
if (!Object.prototype.hasOwnProperty.call(next, key) ||
|
|
857
|
+
!Object.is(prevRecord[key], nextRecord[key])) {
|
|
590
858
|
return false;
|
|
591
859
|
}
|
|
592
860
|
}
|
|
@@ -596,8 +864,23 @@ function applyLinkA11y(element) {
|
|
|
596
864
|
if (!element) {
|
|
597
865
|
return;
|
|
598
866
|
}
|
|
599
|
-
|
|
600
|
-
|
|
867
|
+
// Cross-realm safety (audit-2026-05-17 §5 HIGH #4):
|
|
868
|
+
// `instanceof HTMLAnchorElement` compares against the constructor from
|
|
869
|
+
// the CURRENT realm. An element created in a different window (iframe
|
|
870
|
+
// contentDocument, micro-frontend, embedded widget) fails the check
|
|
871
|
+
// even when it IS a real anchor — the helper would then inject
|
|
872
|
+
// role="link" + tabindex="0" on top of native anchor semantics,
|
|
873
|
+
// breaking screen reader output ("link link") and focus order.
|
|
874
|
+
//
|
|
875
|
+
// tagName is realm-agnostic and is uppercase for HTML-namespaced
|
|
876
|
+
// elements in any document. SVG `<a>` has lowercase tagName plus a
|
|
877
|
+
// different prototype (SVGAElement) — skipping it here is wrong by
|
|
878
|
+
// accident: SVG anchors don't have keyboard activation semantics the
|
|
879
|
+
// helper would add. But they also don't reach this helper in
|
|
880
|
+
// practice (router Link components emit HTML anchors). Lock the
|
|
881
|
+
// uppercase compare to keep the contract narrow.
|
|
882
|
+
const tag = element.tagName;
|
|
883
|
+
if (tag === "A" || tag === "BUTTON") {
|
|
601
884
|
return;
|
|
602
885
|
}
|
|
603
886
|
if (!element.hasAttribute("role")) {
|
|
@@ -608,6 +891,65 @@ function applyLinkA11y(element) {
|
|
|
608
891
|
}
|
|
609
892
|
}
|
|
610
893
|
|
|
894
|
+
/**
|
|
895
|
+
* Shared installation helpers for `provideRealRouter` and
|
|
896
|
+
* `provideRealRouterFactory`. Must be called inside the body of a
|
|
897
|
+
* `provideEnvironmentInitializer(() => { ... })` callback so the active
|
|
898
|
+
* injection context resolves `ROUTER`, `ApplicationRef`, and `DestroyRef`.
|
|
899
|
+
*
|
|
900
|
+
* Closes review-2026-05-10 §8.1 MED — eliminates duplicate wiring between
|
|
901
|
+
* `providers.ts` and `providersFactory.ts` (high drift risk noted in the
|
|
902
|
+
* audit: the comment blocks were identical down to the punctuation).
|
|
903
|
+
*/
|
|
904
|
+
function installScrollRestoration(options) {
|
|
905
|
+
const router = inject(ROUTER);
|
|
906
|
+
const sr = createScrollRestoration(router, options);
|
|
907
|
+
inject(DestroyRef).onDestroy(() => {
|
|
908
|
+
sr.destroy();
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
function installViewTransitions() {
|
|
912
|
+
const router = inject(ROUTER);
|
|
913
|
+
// Feature-detect `document.startViewTransition` once at install time. The
|
|
914
|
+
// `appRef.tick()` listener exists ONLY to feed Angular's zoneless CD into
|
|
915
|
+
// the VT utility's `setTimeout(0)`-driven snapshot capture (see comment
|
|
916
|
+
// below). When `startViewTransition` is unavailable (Firefox as of 2026-04,
|
|
917
|
+
// SSR, older browsers), `createViewTransitions` short-circuits to its
|
|
918
|
+
// frozen NOOP_INSTANCE — no leave subscriber registered, no
|
|
919
|
+
// `setTimeout(0)` invariant to satisfy. Installing the per-navigation
|
|
920
|
+
// tick listener anyway would force a synchronous CD pass on every
|
|
921
|
+
// navigation with zero benefit, doubling CD work in zoneless apps.
|
|
922
|
+
// Closes review-2026-05-10 §8.2 MED (view-transitions hot path).
|
|
923
|
+
const vtAvailable = typeof document !== "undefined" &&
|
|
924
|
+
typeof document.startViewTransition === "function";
|
|
925
|
+
let offTick;
|
|
926
|
+
if (vtAvailable) {
|
|
927
|
+
// Force synchronous change detection on every transition success BEFORE
|
|
928
|
+
// the VT utility resolves its deferred. The utility uses `setTimeout(0)`
|
|
929
|
+
// to release the new-snapshot capture, which is load-bearing because
|
|
930
|
+
// Chromium blocks rAF callbacks while VT sits in the
|
|
931
|
+
// `update-callback-called` phase. Angular's zoneless CD is rAF-driven by
|
|
932
|
+
// default — without this synchronous tick the new DOM is not committed
|
|
933
|
+
// when the browser captures the new snapshot, so old and new snapshots
|
|
934
|
+
// end up identical and animations finish in ~0 ms with no visible work
|
|
935
|
+
// (the inner-route `products.list ↔ products.detail` morph in the
|
|
936
|
+
// example app was the canary).
|
|
937
|
+
//
|
|
938
|
+
// Subscribers fire in registration order; this one runs BEFORE
|
|
939
|
+
// `createViewTransitions` registers its own subscriber, guaranteeing CD
|
|
940
|
+
// completes first.
|
|
941
|
+
const appRef = inject(ApplicationRef);
|
|
942
|
+
offTick = router.subscribe(() => {
|
|
943
|
+
appRef.tick();
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
const vt = createViewTransitions(router);
|
|
947
|
+
inject(DestroyRef).onDestroy(() => {
|
|
948
|
+
offTick?.();
|
|
949
|
+
vt.destroy();
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
611
953
|
/** Must be called within an injection context (constructor, field initializer, runInInjectionContext). */
|
|
612
954
|
function sourceToSignal(source) {
|
|
613
955
|
const sig = signal(source.getSnapshot(), ...(ngDevMode ? [{ debugName: "sig" }] : /* istanbul ignore next */ []));
|
|
@@ -616,8 +958,17 @@ function sourceToSignal(source) {
|
|
|
616
958
|
sig.set(source.getSnapshot());
|
|
617
959
|
});
|
|
618
960
|
destroyRef.onDestroy(() => {
|
|
619
|
-
|
|
620
|
-
|
|
961
|
+
// `try/finally` guarantees `source.destroy()` runs even if `unsubscribe`
|
|
962
|
+
// throws. Cached sources from `@real-router/sources` keep `destroy()` as
|
|
963
|
+
// a no-op (so they survive multi-consumer teardown), but non-cached
|
|
964
|
+
// sources rely on this call to release their router subscription —
|
|
965
|
+
// skipping it on an unsubscribe throw would leak the listener.
|
|
966
|
+
try {
|
|
967
|
+
unsubscribe();
|
|
968
|
+
}
|
|
969
|
+
finally {
|
|
970
|
+
source.destroy();
|
|
971
|
+
}
|
|
621
972
|
});
|
|
622
973
|
return sig.asReadonly();
|
|
623
974
|
}
|
|
@@ -627,6 +978,11 @@ const NAVIGATOR = new InjectionToken("NAVIGATOR");
|
|
|
627
978
|
const ROUTE = new InjectionToken("ROUTE");
|
|
628
979
|
function provideRealRouter(router, options) {
|
|
629
980
|
const navigator = getNavigator(router);
|
|
981
|
+
// `Parameters<typeof makeEnvironmentProviders>[0]` is the actual union
|
|
982
|
+
// `(Provider | EnvironmentProviders | EnvironmentProviders[])[]` —
|
|
983
|
+
// `provideEnvironmentInitializer()` returns `EnvironmentProviders`, so the
|
|
984
|
+
// narrower `Provider[]` would force a cast at every push (review §8a — the
|
|
985
|
+
// proposed Provider[] swap was retracted after discovering this).
|
|
630
986
|
const providers = [
|
|
631
987
|
{ provide: ROUTER, useValue: router },
|
|
632
988
|
{ provide: NAVIGATOR, useValue: navigator },
|
|
@@ -641,66 +997,213 @@ function provideRealRouter(router, options) {
|
|
|
641
997
|
if (options?.scrollRestoration) {
|
|
642
998
|
const scrollOpts = options.scrollRestoration;
|
|
643
999
|
providers.push(provideEnvironmentInitializer(() => {
|
|
644
|
-
|
|
645
|
-
inject(DestroyRef).onDestroy(() => {
|
|
646
|
-
sr.destroy();
|
|
647
|
-
});
|
|
1000
|
+
installScrollRestoration(scrollOpts);
|
|
648
1001
|
}));
|
|
649
1002
|
}
|
|
650
1003
|
if (options?.viewTransitions === true) {
|
|
1004
|
+
providers.push(provideEnvironmentInitializer(installViewTransitions));
|
|
1005
|
+
}
|
|
1006
|
+
return makeEnvironmentProviders(providers);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* `TransferState` key carrying the SSR-resolved router state from server to
|
|
1011
|
+
* client as an XSS-safe JSON string (produced by `serializeRouterState`).
|
|
1012
|
+
* Populated server-side by the `provideAppInitializer` callback after
|
|
1013
|
+
* `router.start()` resolves; consumed client-side after hydration. Mirrors the
|
|
1014
|
+
* `<script>window.__SSR_STATE__ = …</script>` pattern used by every other
|
|
1015
|
+
* adapter — Angular's idiomatic transport is `TransferState` (#599).
|
|
1016
|
+
*
|
|
1017
|
+
* Stored as `string`: `serializeRouterState(state)` already produces JSON;
|
|
1018
|
+
* `hydrateRouter(router, json)` accepts a JSON string and parses it once
|
|
1019
|
+
* internally. Storing the parsed object would force a double round-trip
|
|
1020
|
+
* (TransferState wraps every value in JSON for transport).
|
|
1021
|
+
*
|
|
1022
|
+
* Internal implementation detail. Not re-exported.
|
|
1023
|
+
*/
|
|
1024
|
+
const ROUTER_STATE_KEY = makeStateKey("@real-router/angular:ssrState");
|
|
1025
|
+
/**
|
|
1026
|
+
* `provideRealRouterFactory` — environment providers for SSR / SSG scenarios.
|
|
1027
|
+
*
|
|
1028
|
+
* Unlike `provideRealRouter(router)` (single instance via `useValue`), this
|
|
1029
|
+
* factory uses `useFactory` to produce a per-request router clone:
|
|
1030
|
+
*
|
|
1031
|
+
* 1. Reads Angular's `REQUEST` token (`{ optional: true }`).
|
|
1032
|
+
* 2. Calls `cloneRouter(baseRouter, deps?.(request))` to create a request-scoped clone.
|
|
1033
|
+
* 3. Applies plugins (`plugins` array or `plugins(request)` factory).
|
|
1034
|
+
* 4. Registers `provideAppInitializer` that calls `await router.start(url)`.
|
|
1035
|
+
* 5. Schedules `router.dispose()` via `DestroyRef.onDestroy` — the request
|
|
1036
|
+
* Injector is destroyed after the response is sent, releasing all
|
|
1037
|
+
* subscriptions and plugins.
|
|
1038
|
+
*
|
|
1039
|
+
* Use cases:
|
|
1040
|
+
* - Angular SSR with `@angular/ssr` (`outputMode: "server"`).
|
|
1041
|
+
* - SSG build-time render via `renderApplication` + `platformProviders` `REQUEST` mock.
|
|
1042
|
+
* - Multi-tenant request-scoped routing.
|
|
1043
|
+
*
|
|
1044
|
+
* Existing single-instance scenarios (SPA, SSG client after hydration) continue
|
|
1045
|
+
* to use `provideRealRouter(router)` — both APIs ship in parallel.
|
|
1046
|
+
*
|
|
1047
|
+
* @param options - Factory configuration — see `RealRouterFactoryOptions`.
|
|
1048
|
+
* @returns `EnvironmentProviders` to spread into `ApplicationConfig.providers`.
|
|
1049
|
+
*/
|
|
1050
|
+
function provideRealRouterFactory(options) {
|
|
1051
|
+
const { baseRouter, plugins, deps, scrollRestoration, viewTransitions } = options;
|
|
1052
|
+
const providers = [
|
|
1053
|
+
{
|
|
1054
|
+
provide: ROUTER,
|
|
1055
|
+
useFactory: () => {
|
|
1056
|
+
const request = inject(REQUEST, { optional: true });
|
|
1057
|
+
const requestDeps = deps?.(request);
|
|
1058
|
+
const router = cloneRouter(baseRouter, requestDeps);
|
|
1059
|
+
const pluginList = typeof plugins === "function" ? plugins(request) : plugins;
|
|
1060
|
+
if (pluginList && pluginList.length > 0) {
|
|
1061
|
+
// Variadic — `usePlugin` accepts `(PluginFactory<D> | false | null | undefined)[]`.
|
|
1062
|
+
router.usePlugin(...pluginList);
|
|
1063
|
+
}
|
|
1064
|
+
// Per-request cleanup. The application Injector is destroyed:
|
|
1065
|
+
// - On server: after `writeResponseToNodeResponse` finishes the response
|
|
1066
|
+
// (request scope ends).
|
|
1067
|
+
// - On client: at `ApplicationRef.destroy` (rare in SPA, common in TestBed).
|
|
1068
|
+
// - In SSG build: after each `renderApplication` resolves.
|
|
1069
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1070
|
+
router.dispose();
|
|
1071
|
+
});
|
|
1072
|
+
return router;
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
provide: NAVIGATOR,
|
|
1077
|
+
useFactory: () => getNavigator(inject(ROUTER)),
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
provide: ROUTE,
|
|
1081
|
+
useFactory: () => {
|
|
1082
|
+
const router = inject(ROUTER);
|
|
1083
|
+
return {
|
|
1084
|
+
routeState: sourceToSignal(createRouteSource(router)),
|
|
1085
|
+
navigator: inject(NAVIGATOR),
|
|
1086
|
+
};
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
// Async bootstrap — runs before the first component renders. Three
|
|
1090
|
+
// branches based on TransferState population:
|
|
1091
|
+
//
|
|
1092
|
+
// 1. **Client after hydration** — server populated TransferState with
|
|
1093
|
+
// the SSR-resolved router state. Consume it via `hydrateRouter`,
|
|
1094
|
+
// which deposits the parsed state into the one-shot
|
|
1095
|
+
// `RouterInternals.hydrationState` scratchpad before invoking
|
|
1096
|
+
// `router.start(state.path)`. SSR loader plugins
|
|
1097
|
+
// (`@real-router/ssr-data-plugin`, `@real-router/rsc-server-plugin`)
|
|
1098
|
+
// read the scratchpad and skip the loader on first paint — parity
|
|
1099
|
+
// with the other 5 adapters that consume `<script>__SSR_STATE__</script>` (#596, #599).
|
|
1100
|
+
//
|
|
1101
|
+
// 2. **Server / SSG** — TransferState empty; run the regular
|
|
1102
|
+
// `router.start(path)`. After it resolves, write the serialized
|
|
1103
|
+
// state back into TransferState so the matching client run lands
|
|
1104
|
+
// in branch 1. Angular's `TransferState` infrastructure
|
|
1105
|
+
// (provided by `provideClientHydration()`) carries this blob to
|
|
1106
|
+
// the client as a `<script id="ng-state">` payload.
|
|
1107
|
+
//
|
|
1108
|
+
// 3. **Pure CSR** — TransferState empty (never populated by a server
|
|
1109
|
+
// pass), and `inject(REQUEST, { optional: true })` returns null.
|
|
1110
|
+
// Falls into the same `router.start(path)` branch as server-side
|
|
1111
|
+
// but skips the TransferState write (no client to hand off to).
|
|
1112
|
+
//
|
|
1113
|
+
// Errors propagate (Option A from RFC §10): the bootstrap fails and the
|
|
1114
|
+
// server returns 500. Custom error pages should be wired via
|
|
1115
|
+
// `RouterErrorBoundary` on subsequent renders.
|
|
1116
|
+
provideAppInitializer(async () => {
|
|
1117
|
+
const router = inject(ROUTER);
|
|
1118
|
+
const request = inject(REQUEST, { optional: true });
|
|
1119
|
+
const transferState = inject(TransferState);
|
|
1120
|
+
const ssrJson = transferState.get(ROUTER_STATE_KEY, null);
|
|
1121
|
+
if (ssrJson !== null) {
|
|
1122
|
+
// Branch 1: client after hydration — reuse server-resolved state.
|
|
1123
|
+
await hydrateRouter(router, ssrJson);
|
|
1124
|
+
// One-shot semantic, parity with `delete window.__SSR_STATE__`.
|
|
1125
|
+
transferState.remove(ROUTER_STATE_KEY);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
// Branches 2 & 3: regular start.
|
|
1129
|
+
// Browser-plugin's `start` interceptor (when registered) wraps this call
|
|
1130
|
+
// with location-derived path. We always pass an explicit string — the
|
|
1131
|
+
// interceptor uses the explicit value because `next(path ?? location)`
|
|
1132
|
+
// short-circuits when `path` is non-nullish.
|
|
1133
|
+
const path = deriveStartPath(request);
|
|
1134
|
+
const state = await router.start(path);
|
|
1135
|
+
if (request !== null) {
|
|
1136
|
+
// Branch 2: running inside `@angular/ssr`'s request handler — write
|
|
1137
|
+
// serialized state to TransferState so the matching client run can
|
|
1138
|
+
// skip the loader on first paint.
|
|
1139
|
+
transferState.set(ROUTER_STATE_KEY, serializeRouterState(state));
|
|
1140
|
+
}
|
|
1141
|
+
}),
|
|
1142
|
+
];
|
|
1143
|
+
if (scrollRestoration) {
|
|
651
1144
|
providers.push(provideEnvironmentInitializer(() => {
|
|
652
|
-
|
|
653
|
-
// Force synchronous change detection on every transition success
|
|
654
|
-
// BEFORE the VT utility resolves its deferred. The utility uses
|
|
655
|
-
// `setTimeout(0)` to release the new-snapshot capture, which is
|
|
656
|
-
// load-bearing because Chromium blocks rAF callbacks while VT sits
|
|
657
|
-
// in the `update-callback-called` phase. Angular's zoneless CD is
|
|
658
|
-
// rAF-driven by default — without this synchronous tick the new
|
|
659
|
-
// DOM is not committed when the browser captures the new snapshot,
|
|
660
|
-
// so old and new snapshots end up identical and animations finish
|
|
661
|
-
// in ~0 ms with no visible work (the inner-route `products.list ↔
|
|
662
|
-
// products.detail` morph in the example example was the canary).
|
|
663
|
-
// Subscribers fire in registration order; this one runs BEFORE
|
|
664
|
-
// `createViewTransitions` registers its own subscriber,
|
|
665
|
-
// guaranteeing CD completes first.
|
|
666
|
-
const offTick = router.subscribe(() => {
|
|
667
|
-
appRef.tick();
|
|
668
|
-
});
|
|
669
|
-
const vt = createViewTransitions(router);
|
|
670
|
-
inject(DestroyRef).onDestroy(() => {
|
|
671
|
-
offTick();
|
|
672
|
-
vt.destroy();
|
|
673
|
-
});
|
|
1145
|
+
installScrollRestoration(scrollRestoration);
|
|
674
1146
|
}));
|
|
675
1147
|
}
|
|
1148
|
+
if (viewTransitions === true) {
|
|
1149
|
+
providers.push(provideEnvironmentInitializer(installViewTransitions));
|
|
1150
|
+
}
|
|
676
1151
|
return makeEnvironmentProviders(providers);
|
|
677
1152
|
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Derive the path passed to `router.start(path)`:
|
|
1155
|
+
* - Server / SSG: `request.url` → pathname + search.
|
|
1156
|
+
* - Client: `window.location` if available.
|
|
1157
|
+
* - Fallback: `"/"` (only reachable in synthetic non-browser non-SSR setups).
|
|
1158
|
+
*/
|
|
1159
|
+
function deriveStartPath(request) {
|
|
1160
|
+
if (request) {
|
|
1161
|
+
const url = new URL(request.url);
|
|
1162
|
+
return url.pathname + url.search;
|
|
1163
|
+
}
|
|
1164
|
+
if (typeof globalThis.window !== "undefined") {
|
|
1165
|
+
return globalThis.location.pathname + globalThis.location.search;
|
|
1166
|
+
}
|
|
1167
|
+
return "/";
|
|
1168
|
+
}
|
|
678
1169
|
|
|
679
1170
|
function injectOrThrow(token, fnName) {
|
|
680
1171
|
const value = inject(token, { optional: true });
|
|
681
|
-
|
|
1172
|
+
// Explicit null / undefined check — falsy guard would misfire on
|
|
1173
|
+
// legitimately falsy values (`0`, `""`, `false`) if the token were ever
|
|
1174
|
+
// typed for primitives. Today all our tokens hold object instances, but
|
|
1175
|
+
// pinning the check keeps the function safe for future typing changes.
|
|
1176
|
+
if (value === null || value === undefined) {
|
|
682
1177
|
throw new Error(`${fnName} must be used within a provideRealRouter context`);
|
|
683
1178
|
}
|
|
684
1179
|
return value;
|
|
685
1180
|
}
|
|
686
1181
|
|
|
687
1182
|
function injectRouter() {
|
|
1183
|
+
assertInInjectionContext(injectRouter);
|
|
688
1184
|
return injectOrThrow(ROUTER, "injectRouter");
|
|
689
1185
|
}
|
|
690
1186
|
|
|
691
1187
|
function injectNavigator() {
|
|
1188
|
+
assertInInjectionContext(injectNavigator);
|
|
692
1189
|
return injectOrThrow(NAVIGATOR, "injectNavigator");
|
|
693
1190
|
}
|
|
694
1191
|
|
|
695
1192
|
function injectRoute() {
|
|
1193
|
+
assertInInjectionContext(injectRoute);
|
|
696
1194
|
const signals = injectOrThrow(ROUTE, "injectRoute");
|
|
697
|
-
|
|
1195
|
+
// Read the snapshot once: the signal is reactive, but the throw-guard
|
|
1196
|
+
// and any future use of the snapshot within this call should observe the
|
|
1197
|
+
// SAME value to avoid races.
|
|
1198
|
+
const snapshot = signals.routeState();
|
|
1199
|
+
if (!snapshot.route) {
|
|
698
1200
|
throw new Error("injectRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?");
|
|
699
1201
|
}
|
|
700
1202
|
return signals;
|
|
701
1203
|
}
|
|
702
1204
|
|
|
703
1205
|
function injectRouteNode(nodeName) {
|
|
1206
|
+
assertInInjectionContext(injectRouteNode);
|
|
704
1207
|
const router = injectRouter();
|
|
705
1208
|
const navigator = getNavigator(router);
|
|
706
1209
|
const source = createRouteNodeSource(router, nodeName);
|
|
@@ -709,26 +1212,37 @@ function injectRouteNode(nodeName) {
|
|
|
709
1212
|
}
|
|
710
1213
|
|
|
711
1214
|
function injectRouteUtils() {
|
|
1215
|
+
assertInInjectionContext(injectRouteUtils);
|
|
712
1216
|
const router = injectRouter();
|
|
713
1217
|
return getRouteUtils(getPluginApi(router).getTree());
|
|
714
1218
|
}
|
|
715
1219
|
|
|
716
1220
|
function injectRouterTransition() {
|
|
1221
|
+
assertInInjectionContext(injectRouterTransition);
|
|
717
1222
|
const router = injectRouter();
|
|
718
1223
|
const source = getTransitionSource(router);
|
|
719
1224
|
return sourceToSignal(source);
|
|
720
1225
|
}
|
|
721
1226
|
|
|
1227
|
+
/**
|
|
1228
|
+
* Build the `options` literal for `createActiveRouteSource` while honoring
|
|
1229
|
+
* `exactOptionalPropertyTypes` — the type forbids passing `{ hash: undefined }`
|
|
1230
|
+
* literally (#532), so callers must conditionally include the `hash` key only
|
|
1231
|
+
* when a value was provided.
|
|
1232
|
+
*
|
|
1233
|
+
* Used by `RealLink`, `RealLinkActive`, and `injectIsActiveRoute` — extracted
|
|
1234
|
+
* from three identical ternaries (review-2026-05-16 §8a LOW).
|
|
1235
|
+
*/
|
|
1236
|
+
function buildActiveRouteOptions(strict, ignoreQueryParams, hash) {
|
|
1237
|
+
return hash === undefined
|
|
1238
|
+
? { strict, ignoreQueryParams }
|
|
1239
|
+
: { strict, ignoreQueryParams, hash };
|
|
1240
|
+
}
|
|
1241
|
+
|
|
722
1242
|
function injectIsActiveRoute(routeName, params, options) {
|
|
1243
|
+
assertInInjectionContext(injectIsActiveRoute);
|
|
723
1244
|
const router = injectRouter();
|
|
724
|
-
const
|
|
725
|
-
const ignoreQueryParams = options?.ignoreQueryParams ?? true;
|
|
726
|
-
const hash = options?.hash;
|
|
727
|
-
// exactOptionalPropertyTypes forbids `{ hash: undefined }` literally — pass
|
|
728
|
-
// the field only when a value was provided. (#532)
|
|
729
|
-
const source = createActiveRouteSource(router, routeName, params, hash === undefined
|
|
730
|
-
? { strict, ignoreQueryParams }
|
|
731
|
-
: { strict, ignoreQueryParams, hash });
|
|
1245
|
+
const source = createActiveRouteSource(router, routeName, params, buildActiveRouteOptions(options?.strict ?? false, options?.ignoreQueryParams ?? true, options?.hash));
|
|
732
1246
|
return sourceToSignal(source);
|
|
733
1247
|
}
|
|
734
1248
|
|
|
@@ -870,7 +1384,6 @@ function injectRouteEnter(handler, options) {
|
|
|
870
1384
|
assertInInjectionContext(injectRouteEnter);
|
|
871
1385
|
const { routeState } = injectRoute();
|
|
872
1386
|
const skipSameRoute = options?.skipSameRoute ?? true;
|
|
873
|
-
let lastHandledRoute = null;
|
|
874
1387
|
effect(() => {
|
|
875
1388
|
const { route, previousRoute } = routeState();
|
|
876
1389
|
// Early-exit guards, top-down:
|
|
@@ -880,23 +1393,19 @@ function injectRouteEnter(handler, options) {
|
|
|
880
1393
|
// - **Skip-same-route**: query-only navigations have
|
|
881
1394
|
// `transition.from === route.name`. Opt-out via
|
|
882
1395
|
// `skipSameRoute: false`.
|
|
883
|
-
// - **Defensive dedupe + missing `previousRoute`**: same `route`
|
|
884
|
-
// ref between effect re-runs is unexpected on Angular (the
|
|
885
|
-
// signal only fires on real reference changes); `!previousRoute`
|
|
886
|
-
// is unreachable once `transition.from` is set (core populates
|
|
887
|
-
// them together). Both kept for parity with React; v8-ignored.
|
|
888
1396
|
if (!route.transition.from) {
|
|
889
1397
|
return;
|
|
890
1398
|
}
|
|
891
1399
|
if (skipSameRoute && route.transition.from === route.name) {
|
|
892
1400
|
return;
|
|
893
1401
|
}
|
|
894
|
-
|
|
895
|
-
|
|
1402
|
+
// `previousRoute` is guaranteed populated whenever `route.transition.from`
|
|
1403
|
+
// is set — core writes them together. The dead-code throw-guard that used
|
|
1404
|
+
// to live here (review §8a LOW) is removed; the narrowing below is the
|
|
1405
|
+
// type-safe equivalent and avoids the no-non-null-assertion lint.
|
|
1406
|
+
if (!previousRoute) {
|
|
896
1407
|
return;
|
|
897
1408
|
}
|
|
898
|
-
/* v8 ignore stop */
|
|
899
|
-
lastHandledRoute = route;
|
|
900
1409
|
handler({ route, previousRoute });
|
|
901
1410
|
});
|
|
902
1411
|
}
|
|
@@ -904,60 +1413,131 @@ function injectRouteEnter(handler, options) {
|
|
|
904
1413
|
class RouteMatch {
|
|
905
1414
|
routeMatch = input.required(...(ngDevMode ? [{ debugName: "routeMatch" }] : /* istanbul ignore next */ []));
|
|
906
1415
|
templateRef = inject(TemplateRef);
|
|
907
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
908
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
1416
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouteMatch, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1417
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RouteMatch, isStandalone: true, selector: "ng-template[routeMatch]", inputs: { routeMatch: { classPropertyName: "routeMatch", publicName: "routeMatch", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
909
1418
|
}
|
|
910
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1419
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouteMatch, decorators: [{
|
|
911
1420
|
type: Directive,
|
|
912
1421
|
args: [{ selector: "ng-template[routeMatch]" }]
|
|
913
1422
|
}], propDecorators: { routeMatch: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeMatch", required: true }] }] } });
|
|
914
1423
|
|
|
915
1424
|
class RouteNotFound {
|
|
916
1425
|
templateRef = inject(TemplateRef);
|
|
917
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
918
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.
|
|
1426
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouteNotFound, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1427
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RouteNotFound, isStandalone: true, selector: "ng-template[routeNotFound]", ngImport: i0 });
|
|
919
1428
|
}
|
|
920
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1429
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouteNotFound, decorators: [{
|
|
921
1430
|
type: Directive,
|
|
922
1431
|
args: [{ selector: "ng-template[routeNotFound]" }]
|
|
923
1432
|
}] });
|
|
924
1433
|
|
|
925
1434
|
class RouteSelf {
|
|
926
1435
|
templateRef = inject(TemplateRef);
|
|
927
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
928
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.
|
|
1436
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouteSelf, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1437
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RouteSelf, isStandalone: true, selector: "ng-template[routeSelf]", ngImport: i0 });
|
|
929
1438
|
}
|
|
930
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1439
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouteSelf, decorators: [{
|
|
931
1440
|
type: Directive,
|
|
932
1441
|
args: [{ selector: "ng-template[routeSelf]" }]
|
|
933
1442
|
}] });
|
|
934
1443
|
|
|
935
|
-
|
|
1444
|
+
/**
|
|
1445
|
+
* Subscribe a `RouterSource<T>` to a write-callback and return a cleanup
|
|
1446
|
+
* function. The shape is the per-effect-run pattern that `RealLink`,
|
|
1447
|
+
* `RealLinkActive`, and `RouteView` all share inside their constructor
|
|
1448
|
+
* `effect(...)` (review-2026-05-16 §8a MEDIUM — identical 8-line block
|
|
1449
|
+
* repeated in 3 directives):
|
|
1450
|
+
*
|
|
1451
|
+
* 1. Read initial snapshot and apply it via `onSnapshot(snap)`.
|
|
1452
|
+
* 2. Subscribe — every subsequent emission calls `onSnapshot(snap)` again.
|
|
1453
|
+
* 3. Return a cleanup that unsubscribes and destroys the source. For
|
|
1454
|
+
* cached factories from `@real-router/sources` (`createActiveRouteSource`,
|
|
1455
|
+
* `createRouteNodeSource`, `getTransitionSource`, `getErrorSource`,
|
|
1456
|
+
* `createDismissableError`) `destroy()` is a no-op on the shared
|
|
1457
|
+
* wrapper, so this helper is safe to invoke from rapid effect re-runs
|
|
1458
|
+
* under signal-input changes.
|
|
1459
|
+
*
|
|
1460
|
+
* Callers pass the result to `onCleanup(...)` from Angular's `effect()`.
|
|
1461
|
+
*
|
|
1462
|
+
* @example
|
|
1463
|
+
* ```ts
|
|
1464
|
+
* effect((onCleanup) => {
|
|
1465
|
+
* const source = createActiveRouteSource(router, routeName(), params());
|
|
1466
|
+
* onCleanup(
|
|
1467
|
+
* subscribeSourceToSignal(source, (snap) => {
|
|
1468
|
+
* this.isActive.set(snap);
|
|
1469
|
+
* this.updateDom();
|
|
1470
|
+
* }),
|
|
1471
|
+
* );
|
|
1472
|
+
* });
|
|
1473
|
+
* ```
|
|
1474
|
+
*/
|
|
1475
|
+
function subscribeSourceToSignal(source, onSnapshot) {
|
|
1476
|
+
onSnapshot(source.getSnapshot());
|
|
1477
|
+
const unsub = source.subscribe(() => {
|
|
1478
|
+
onSnapshot(source.getSnapshot());
|
|
1479
|
+
});
|
|
1480
|
+
return () => {
|
|
1481
|
+
unsub();
|
|
1482
|
+
source.destroy();
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const EMPTY_SNAPSHOT = {
|
|
936
1487
|
route: undefined,
|
|
937
1488
|
previousRoute: undefined,
|
|
938
|
-
}
|
|
1489
|
+
};
|
|
939
1490
|
class RouteView {
|
|
940
1491
|
nodeName = input("", { ...(ngDevMode ? { debugName: "nodeName" } : /* istanbul ignore next */ {}), alias: "routeNode" });
|
|
941
1492
|
matches = contentChildren(RouteMatch, { ...(ngDevMode ? { debugName: "matches" } : /* istanbul ignore next */ {}), descendants: true });
|
|
942
1493
|
selfs = contentChildren(RouteSelf, { ...(ngDevMode ? { debugName: "selfs" } : /* istanbul ignore next */ {}), descendants: true });
|
|
943
1494
|
notFounds = contentChildren(RouteNotFound, { ...(ngDevMode ? { debugName: "notFounds" } : /* istanbul ignore next */ {}), descendants: true });
|
|
944
|
-
activeTemplate = computed(() => {
|
|
945
|
-
|
|
946
|
-
|
|
1495
|
+
activeTemplate = computed(() => this.matchedTemplate() ?? this.fallbackTemplate(), ...(ngDevMode ? [{ debugName: "activeTemplate" }] : /* istanbul ignore next */ []));
|
|
1496
|
+
router = injectRouter();
|
|
1497
|
+
routeState = signal(EMPTY_SNAPSHOT, ...(ngDevMode ? [{ debugName: "routeState" }] : /* istanbul ignore next */ []));
|
|
1498
|
+
matchEntries = computed(() => {
|
|
1499
|
+
const nodeName = this.nodeName();
|
|
1500
|
+
return this.matches().map((match) => {
|
|
1501
|
+
const segment = match.routeMatch();
|
|
1502
|
+
return {
|
|
1503
|
+
match,
|
|
1504
|
+
fullSegmentName: nodeName ? `${nodeName}.${segment}` : segment,
|
|
1505
|
+
};
|
|
1506
|
+
});
|
|
1507
|
+
}, ...(ngDevMode ? [{ debugName: "matchEntries" }] : /* istanbul ignore next */ []));
|
|
1508
|
+
// The matched template (Match priority) is independent of the Self /
|
|
1509
|
+
// NotFound fallback chain. Splitting the two paths into separate computeds
|
|
1510
|
+
// localises re-runs: a change to `selfs()` / `notFounds()` no longer
|
|
1511
|
+
// re-evaluates the Match loop (review §8a LOW — RouteView activeTemplate
|
|
1512
|
+
// split).
|
|
1513
|
+
matchedTemplate = computed(() => {
|
|
1514
|
+
const route = this.routeState().route;
|
|
947
1515
|
if (!route) {
|
|
948
1516
|
return null;
|
|
949
1517
|
}
|
|
950
1518
|
const routeName = route.name;
|
|
951
|
-
const
|
|
952
|
-
for (const { match, fullSegmentName } of entries) {
|
|
1519
|
+
for (const { match, fullSegmentName } of this.matchEntries()) {
|
|
953
1520
|
if (startsWithSegment(routeName, fullSegmentName)) {
|
|
954
1521
|
return match.templateRef;
|
|
955
1522
|
}
|
|
956
1523
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1524
|
+
return null;
|
|
1525
|
+
}, ...(ngDevMode ? [{ debugName: "matchedTemplate" }] : /* istanbul ignore next */ []));
|
|
1526
|
+
// Fallback chain — only consulted when `matchedTemplate()` returned `null`.
|
|
1527
|
+
// Template priority: Self → NotFound. Selection rules differ on purpose:
|
|
1528
|
+
// - **Self uses first-wins** (`.at(0)`) for parity with React / Preact /
|
|
1529
|
+
// Solid / Vue, where the first matching `<Self>` token in declaration
|
|
1530
|
+
// order wins.
|
|
1531
|
+
// - **NotFound uses last-wins** (`.at(-1)`) intentionally — the fallback
|
|
1532
|
+
// should be the most-recently-declared template so that consumers can
|
|
1533
|
+
// override an inherited `<ng-template routeNotFound>` simply by
|
|
1534
|
+
// re-declaring it lower in the projected content.
|
|
1535
|
+
fallbackTemplate = computed(() => {
|
|
1536
|
+
const route = this.routeState().route;
|
|
1537
|
+
if (!route) {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
const routeName = route.name;
|
|
961
1541
|
if (routeName === this.nodeName()) {
|
|
962
1542
|
const first = this.selfs().at(0);
|
|
963
1543
|
if (first) {
|
|
@@ -971,39 +1551,25 @@ class RouteView {
|
|
|
971
1551
|
}
|
|
972
1552
|
}
|
|
973
1553
|
return null;
|
|
974
|
-
}, ...(ngDevMode ? [{ debugName: "
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
};
|
|
983
|
-
});
|
|
984
|
-
}, ...(ngDevMode ? [{ debugName: "matchEntries" }] : /* istanbul ignore next */ []));
|
|
985
|
-
router = injectRouter();
|
|
986
|
-
destroyRef = inject(DestroyRef);
|
|
987
|
-
routeState = signal(EMPTY_SNAPSHOT, ...(ngDevMode ? [{ debugName: "routeState" }] : /* istanbul ignore next */ []));
|
|
988
|
-
ngOnInit() {
|
|
989
|
-
const source = createRouteNodeSource(this.router, this.nodeName());
|
|
990
|
-
this.routeState.set(source.getSnapshot());
|
|
991
|
-
const unsub = source.subscribe(() => {
|
|
992
|
-
this.routeState.set(source.getSnapshot());
|
|
993
|
-
});
|
|
994
|
-
this.destroyRef.onDestroy(() => {
|
|
995
|
-
unsub();
|
|
996
|
-
source.destroy();
|
|
1554
|
+
}, ...(ngDevMode ? [{ debugName: "fallbackTemplate" }] : /* istanbul ignore next */ []));
|
|
1555
|
+
constructor() {
|
|
1556
|
+
// Reactive source-creation effect (#630 fix) — see
|
|
1557
|
+
// `packages/angular/CLAUDE.md` → "Directives use constructor + effect()".
|
|
1558
|
+
effect((onCleanup) => {
|
|
1559
|
+
const source = createRouteNodeSource(this.router, this.nodeName());
|
|
1560
|
+
onCleanup(subscribeSourceToSignal(source, (snap) => {
|
|
1561
|
+
this.routeState.set(snap);
|
|
1562
|
+
}));
|
|
997
1563
|
});
|
|
998
1564
|
}
|
|
999
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1000
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1565
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouteView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1566
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: RouteView, isStandalone: true, selector: "route-view", inputs: { nodeName: { classPropertyName: "nodeName", publicName: "routeNode", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "matches", predicate: RouteMatch, descendants: true, isSignal: true }, { propertyName: "selfs", predicate: RouteSelf, descendants: true, isSignal: true }, { propertyName: "notFounds", predicate: RouteNotFound, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1001
1567
|
@if (activeTemplate()) {
|
|
1002
1568
|
<ng-container [ngTemplateOutlet]="activeTemplate()!" />
|
|
1003
1569
|
}
|
|
1004
1570
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1005
1571
|
}
|
|
1006
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1572
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouteView, decorators: [{
|
|
1007
1573
|
type: Component,
|
|
1008
1574
|
args: [{
|
|
1009
1575
|
selector: "route-view",
|
|
@@ -1014,7 +1580,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1014
1580
|
`,
|
|
1015
1581
|
imports: [NgTemplateOutlet],
|
|
1016
1582
|
}]
|
|
1017
|
-
}], propDecorators: { nodeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeNode", required: false }] }], matches: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteMatch), { ...{ descendants: true }, isSignal: true }] }], selfs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteSelf), { ...{ descendants: true }, isSignal: true }] }], notFounds: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteNotFound), { ...{ descendants: true }, isSignal: true }] }] } });
|
|
1583
|
+
}], ctorParameters: () => [], propDecorators: { nodeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeNode", required: false }] }], matches: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteMatch), { ...{ descendants: true }, isSignal: true }] }], selfs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteSelf), { ...{ descendants: true }, isSignal: true }] }], notFounds: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteNotFound), { ...{ descendants: true }, isSignal: true }] }] } });
|
|
1018
1584
|
|
|
1019
1585
|
class RouterErrorBoundary {
|
|
1020
1586
|
errorTemplate = input(...(ngDevMode ? [undefined, { debugName: "errorTemplate" }] : /* istanbul ignore next */ []));
|
|
@@ -1032,6 +1598,12 @@ class RouterErrorBoundary {
|
|
|
1032
1598
|
router = injectRouter();
|
|
1033
1599
|
snapshot = sourceToSignal(createDismissableError(this.router));
|
|
1034
1600
|
constructor() {
|
|
1601
|
+
// `effect()` registers itself with the current injection context's
|
|
1602
|
+
// `DestroyRef` and tears down automatically when the component is
|
|
1603
|
+
// destroyed. The earlier manual `effectRef.destroy()` wired through
|
|
1604
|
+
// `inject(DestroyRef).onDestroy(...)` duplicated that built-in cleanup
|
|
1605
|
+
// (audit §8.1 LOW — confirmed: no behavior change without the manual
|
|
1606
|
+
// path).
|
|
1035
1607
|
effect(() => {
|
|
1036
1608
|
const snap = this.snapshot();
|
|
1037
1609
|
if (snap.error) {
|
|
@@ -1043,8 +1615,8 @@ class RouterErrorBoundary {
|
|
|
1043
1615
|
}
|
|
1044
1616
|
});
|
|
1045
1617
|
}
|
|
1046
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1047
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1618
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouterErrorBoundary, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1619
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: RouterErrorBoundary, isStandalone: true, selector: "router-error-boundary", inputs: { errorTemplate: { classPropertyName: "errorTemplate", publicName: "errorTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onError: "onError" }, ngImport: i0, template: `
|
|
1048
1620
|
<ng-content />
|
|
1049
1621
|
@if (errorContext() && errorTemplate()) {
|
|
1050
1622
|
<ng-container
|
|
@@ -1054,7 +1626,7 @@ class RouterErrorBoundary {
|
|
|
1054
1626
|
}
|
|
1055
1627
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1056
1628
|
}
|
|
1057
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1629
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RouterErrorBoundary, decorators: [{
|
|
1058
1630
|
type: Component,
|
|
1059
1631
|
args: [{
|
|
1060
1632
|
selector: "router-error-boundary",
|
|
@@ -1078,10 +1650,10 @@ class NavigationAnnouncer {
|
|
|
1078
1650
|
this.announcer.destroy();
|
|
1079
1651
|
});
|
|
1080
1652
|
}
|
|
1081
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1082
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.
|
|
1653
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NavigationAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1654
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: NavigationAnnouncer, isStandalone: true, selector: "navigation-announcer", ngImport: i0, template: "", isInline: true });
|
|
1083
1655
|
}
|
|
1084
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1656
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NavigationAnnouncer, decorators: [{
|
|
1085
1657
|
type: Component,
|
|
1086
1658
|
args: [{
|
|
1087
1659
|
selector: "navigation-announcer",
|
|
@@ -1089,6 +1661,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1089
1661
|
}]
|
|
1090
1662
|
}], ctorParameters: () => [] });
|
|
1091
1663
|
|
|
1664
|
+
const NOOP_CATCH = () => { };
|
|
1092
1665
|
class RealLink {
|
|
1093
1666
|
routeName = input("", ...(ngDevMode ? [{ debugName: "routeName" }] : /* istanbul ignore next */ []));
|
|
1094
1667
|
routeParams = input({}, ...(ngDevMode ? [{ debugName: "routeParams" }] : /* istanbul ignore next */ []));
|
|
@@ -1104,38 +1677,45 @@ class RealLink {
|
|
|
1104
1677
|
*/
|
|
1105
1678
|
hash = input(undefined, ...(ngDevMode ? [{ debugName: "hash" }] : /* istanbul ignore next */ []));
|
|
1106
1679
|
router = injectRouter();
|
|
1107
|
-
destroyRef = inject(DestroyRef);
|
|
1108
1680
|
anchor = inject(ElementRef)
|
|
1109
1681
|
.nativeElement;
|
|
1110
1682
|
isActive = signal(false, ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
|
|
1683
|
+
// `href` is computed from signal inputs only — Angular's default Object.is
|
|
1684
|
+
// equality already collapses repeated `string` results, so no custom
|
|
1685
|
+
// comparator is required (review §8b note 3 — applies after verifying that
|
|
1686
|
+
// `buildHref` returns a primitive).
|
|
1111
1687
|
href = computed(() => {
|
|
1112
1688
|
const hashValue = this.hash();
|
|
1113
1689
|
return buildHref(this.router, this.routeName(), this.routeParams(), hashValue === undefined ? undefined : { hash: hashValue });
|
|
1114
1690
|
}, ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
1115
1691
|
prevActiveClass = "";
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1692
|
+
prevHref = undefined;
|
|
1693
|
+
// Skip-same-value: only re-touch the DOM `class` list when the active state
|
|
1694
|
+
// actually flipped. Without this, every navigation that re-fires the active
|
|
1695
|
+
// source still issues a `classList.toggle` no-op (review §8b MEDIUM).
|
|
1696
|
+
prevActive = undefined;
|
|
1697
|
+
constructor() {
|
|
1698
|
+
// Reactive source-creation effect (#630 fix) — see
|
|
1699
|
+
// `packages/angular/CLAUDE.md` → "Directives use constructor + effect()".
|
|
1700
|
+
// Reading signal inputs inside `effect()` re-creates the active-route
|
|
1701
|
+
// source whenever any input changes; `onCleanup` tears the previous
|
|
1702
|
+
// subscription down.
|
|
1703
|
+
effect((onCleanup) => {
|
|
1704
|
+
const source = createActiveRouteSource(this.router, this.routeName(), this.routeParams(), buildActiveRouteOptions(this.activeStrict(), this.ignoreQueryParams(), this.hash()));
|
|
1705
|
+
onCleanup(subscribeSourceToSignal(source, (snap) => {
|
|
1706
|
+
// Pure-href refresh: when the active flag did not change, only the
|
|
1707
|
+
// href may have moved (e.g. param-only update on a parent route).
|
|
1708
|
+
// Skip the classList work in that branch (review §8b MEDIUM).
|
|
1709
|
+
if (snap === this.prevActive) {
|
|
1710
|
+
this.isActive.set(snap);
|
|
1711
|
+
this.updateHref();
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
this.prevActive = snap;
|
|
1715
|
+
this.isActive.set(snap);
|
|
1716
|
+
this.updateHref();
|
|
1717
|
+
this.updateActiveClass();
|
|
1718
|
+
}));
|
|
1139
1719
|
});
|
|
1140
1720
|
}
|
|
1141
1721
|
onClick(event) {
|
|
@@ -1143,13 +1723,16 @@ class RealLink {
|
|
|
1143
1723
|
return;
|
|
1144
1724
|
}
|
|
1145
1725
|
event.preventDefault();
|
|
1146
|
-
navigateWithHash(this.router, this.routeName(), this.routeParams(), this.hash(), this.routeOptions()).catch(
|
|
1726
|
+
navigateWithHash(this.router, this.routeName(), this.routeParams(), this.hash(), this.routeOptions()).catch(NOOP_CATCH);
|
|
1147
1727
|
}
|
|
1148
|
-
|
|
1728
|
+
updateHref() {
|
|
1149
1729
|
const href = this.href();
|
|
1150
|
-
if (href !== undefined) {
|
|
1730
|
+
if (href !== undefined && href !== this.prevHref) {
|
|
1151
1731
|
this.anchor.setAttribute("href", href);
|
|
1152
1732
|
}
|
|
1733
|
+
this.prevHref = href;
|
|
1734
|
+
}
|
|
1735
|
+
updateActiveClass() {
|
|
1153
1736
|
const activeClass = this.activeClassName();
|
|
1154
1737
|
if (this.prevActiveClass && this.prevActiveClass !== activeClass) {
|
|
1155
1738
|
this.anchor.classList.remove(this.prevActiveClass);
|
|
@@ -1159,10 +1742,10 @@ class RealLink {
|
|
|
1159
1742
|
}
|
|
1160
1743
|
this.prevActiveClass = activeClass;
|
|
1161
1744
|
}
|
|
1162
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1163
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
1745
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RealLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1746
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RealLink, isStandalone: true, selector: "a[realLink]", inputs: { routeName: { classPropertyName: "routeName", publicName: "routeName", isSignal: true, isRequired: false, transformFunction: null }, routeParams: { classPropertyName: "routeParams", publicName: "routeParams", isSignal: true, isRequired: false, transformFunction: null }, routeOptions: { classPropertyName: "routeOptions", publicName: "routeOptions", isSignal: true, isRequired: false, transformFunction: null }, activeClassName: { classPropertyName: "activeClassName", publicName: "activeClassName", isSignal: true, isRequired: false, transformFunction: null }, activeStrict: { classPropertyName: "activeStrict", publicName: "activeStrict", isSignal: true, isRequired: false, transformFunction: null }, ignoreQueryParams: { classPropertyName: "ignoreQueryParams", publicName: "ignoreQueryParams", isSignal: true, isRequired: false, transformFunction: null }, hash: { classPropertyName: "hash", publicName: "hash", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onClick($event)" } }, ngImport: i0 });
|
|
1164
1747
|
}
|
|
1165
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1748
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RealLink, decorators: [{
|
|
1166
1749
|
type: Directive,
|
|
1167
1750
|
args: [{
|
|
1168
1751
|
selector: "a[realLink]",
|
|
@@ -1170,7 +1753,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1170
1753
|
"(click)": "onClick($event)",
|
|
1171
1754
|
},
|
|
1172
1755
|
}]
|
|
1173
|
-
}], propDecorators: { routeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeName", required: false }] }], routeParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeParams", required: false }] }], routeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeOptions", required: false }] }], activeClassName: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeClassName", required: false }] }], activeStrict: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeStrict", required: false }] }], ignoreQueryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreQueryParams", required: false }] }], hash: [{ type: i0.Input, args: [{ isSignal: true, alias: "hash", required: false }] }] } });
|
|
1756
|
+
}], ctorParameters: () => [], propDecorators: { routeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeName", required: false }] }], routeParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeParams", required: false }] }], routeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeOptions", required: false }] }], activeClassName: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeClassName", required: false }] }], activeStrict: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeStrict", required: false }] }], ignoreQueryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreQueryParams", required: false }] }], hash: [{ type: i0.Input, args: [{ isSignal: true, alias: "hash", required: false }] }] } });
|
|
1174
1757
|
|
|
1175
1758
|
class RealLinkActive {
|
|
1176
1759
|
realLinkActive = input("", ...(ngDevMode ? [{ debugName: "realLinkActive" }] : /* istanbul ignore next */ []));
|
|
@@ -1179,26 +1762,29 @@ class RealLinkActive {
|
|
|
1179
1762
|
activeStrict = input(false, ...(ngDevMode ? [{ debugName: "activeStrict" }] : /* istanbul ignore next */ []));
|
|
1180
1763
|
ignoreQueryParams = input(true, ...(ngDevMode ? [{ debugName: "ignoreQueryParams" }] : /* istanbul ignore next */ []));
|
|
1181
1764
|
router = injectRouter();
|
|
1182
|
-
destroyRef = inject(DestroyRef);
|
|
1183
1765
|
element = inject(ElementRef).nativeElement;
|
|
1184
1766
|
isActive = signal(false, ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
|
|
1767
|
+
// Skip-same-value: only touch `classList.toggle` when the active flag
|
|
1768
|
+
// actually flipped. Saves one DOM write per RealLinkActive per unrelated
|
|
1769
|
+
// navigation (review §8b MEDIUM).
|
|
1770
|
+
prevActive = undefined;
|
|
1185
1771
|
constructor() {
|
|
1772
|
+
// One-time a11y setup — doesn't depend on signal inputs, stays in
|
|
1773
|
+
// constructor body. `applyLinkA11y` is idempotent so re-running would
|
|
1774
|
+
// be safe, but we only need it once per element.
|
|
1186
1775
|
applyLinkA11y(this.element);
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
this.destroyRef.onDestroy(() => {
|
|
1200
|
-
unsub();
|
|
1201
|
-
source.destroy();
|
|
1776
|
+
// Reactive source-creation effect (#630 fix) — see
|
|
1777
|
+
// `packages/angular/CLAUDE.md` → "Directives use constructor + effect()".
|
|
1778
|
+
effect((onCleanup) => {
|
|
1779
|
+
const source = createActiveRouteSource(this.router, this.routeName(), this.routeParams(), buildActiveRouteOptions(this.activeStrict(), this.ignoreQueryParams(), undefined));
|
|
1780
|
+
onCleanup(subscribeSourceToSignal(source, (snap) => {
|
|
1781
|
+
if (snap === this.prevActive) {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
this.prevActive = snap;
|
|
1785
|
+
this.isActive.set(snap);
|
|
1786
|
+
this.updateClass();
|
|
1787
|
+
}));
|
|
1202
1788
|
});
|
|
1203
1789
|
}
|
|
1204
1790
|
updateClass() {
|
|
@@ -1208,10 +1794,10 @@ class RealLinkActive {
|
|
|
1208
1794
|
}
|
|
1209
1795
|
this.element.classList.toggle(className, this.isActive());
|
|
1210
1796
|
}
|
|
1211
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1212
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
1797
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RealLinkActive, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1798
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RealLinkActive, isStandalone: true, selector: "[realLinkActive]", inputs: { realLinkActive: { classPropertyName: "realLinkActive", publicName: "realLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routeName: { classPropertyName: "routeName", publicName: "routeName", isSignal: true, isRequired: false, transformFunction: null }, routeParams: { classPropertyName: "routeParams", publicName: "routeParams", isSignal: true, isRequired: false, transformFunction: null }, activeStrict: { classPropertyName: "activeStrict", publicName: "activeStrict", isSignal: true, isRequired: false, transformFunction: null }, ignoreQueryParams: { classPropertyName: "ignoreQueryParams", publicName: "ignoreQueryParams", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1213
1799
|
}
|
|
1214
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1800
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RealLinkActive, decorators: [{
|
|
1215
1801
|
type: Directive,
|
|
1216
1802
|
args: [{ selector: "[realLinkActive]" }]
|
|
1217
1803
|
}], ctorParameters: () => [], propDecorators: { realLinkActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "realLinkActive", required: false }] }], routeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeName", required: false }] }], routeParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeParams", required: false }] }], activeStrict: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeStrict", required: false }] }], ignoreQueryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreQueryParams", required: false }] }] } });
|
|
@@ -1220,5 +1806,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1220
1806
|
* Generated bundle index. Do not edit.
|
|
1221
1807
|
*/
|
|
1222
1808
|
|
|
1223
|
-
export { NAVIGATOR, NavigationAnnouncer, ROUTE, ROUTER, RealLink, RealLinkActive, RouteMatch, RouteNotFound, RouteSelf, RouteView, RouterErrorBoundary, injectIsActiveRoute, injectNavigator, injectRoute, injectRouteEnter, injectRouteExit, injectRouteNode, injectRouteUtils, injectRouter, injectRouterTransition, provideRealRouter, sourceToSignal };
|
|
1809
|
+
export { NAVIGATOR, NavigationAnnouncer, ROUTE, ROUTER, RealLink, RealLinkActive, RouteMatch, RouteNotFound, RouteSelf, RouteView, RouterErrorBoundary, injectIsActiveRoute, injectNavigator, injectRoute, injectRouteEnter, injectRouteExit, injectRouteNode, injectRouteUtils, injectRouter, injectRouterTransition, provideRealRouter, provideRealRouterFactory, sourceToSignal };
|
|
1224
1810
|
//# sourceMappingURL=real-router-angular.mjs.map
|