@real-router/angular 0.8.1 → 0.10.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 +184 -5
- package/dist/README.md +184 -5
- 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 +773 -180
- 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 +17 -10
- 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 +179 -23
- 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,25 +379,36 @@ 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
|
-
// Single rAF so DOM is committed before we read anchors / write scroll.
|
|
298
|
-
// Guard against destroy() racing with the callback.
|
|
299
387
|
requestAnimationFrame(() => {
|
|
300
388
|
if (destroyed) {
|
|
301
389
|
return;
|
|
302
390
|
}
|
|
303
|
-
if (mode === "top"
|
|
391
|
+
if (mode === "top") {
|
|
304
392
|
scrollToHashOrTop(route);
|
|
305
393
|
return;
|
|
306
394
|
}
|
|
307
|
-
if (nav
|
|
395
|
+
if (route.transition.replace || nav?.navigationType === "replace") {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
// Both arms are required: `transition.reload` only fires for programmatic
|
|
399
|
+
// `router.navigate({reload:true})`. F5 under navigation-plugin primes
|
|
400
|
+
// `nav.navigationType === "reload"` via #531 getActivationType but leaves
|
|
401
|
+
// opts.reload undefined, so dropping the plugin arm would regress F5
|
|
402
|
+
// scroll-restore. Same belt-and-suspenders pattern is used for replace
|
|
403
|
+
// above. Browser-plugin's F5 is not covered (no priming, out of scope).
|
|
404
|
+
if (route.transition.reload || nav?.navigationType === "reload") {
|
|
405
|
+
const key = safeKeyOf(route);
|
|
406
|
+
writePos(key === null ? 0 : (loadStore()[key] ?? 0));
|
|
308
407
|
return;
|
|
309
408
|
}
|
|
310
|
-
if (nav
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
writePos(loadStore()[keyOf(route)] ?? 0);
|
|
409
|
+
if (nav?.direction === "back" || nav?.navigationType === "traverse") {
|
|
410
|
+
const key = safeKeyOf(route);
|
|
411
|
+
writePos(key === null ? 0 : (loadStore()[key] ?? 0));
|
|
314
412
|
return;
|
|
315
413
|
}
|
|
316
414
|
scrollToHashOrTop(route);
|
|
@@ -319,7 +417,10 @@ function createScrollRestoration(router, options) {
|
|
|
319
417
|
const onPageHide = () => {
|
|
320
418
|
const current = router.getState();
|
|
321
419
|
if (current) {
|
|
322
|
-
|
|
420
|
+
const key = safeKeyOf(current);
|
|
421
|
+
if (key !== null) {
|
|
422
|
+
putPos(key, readPos());
|
|
423
|
+
}
|
|
323
424
|
}
|
|
324
425
|
};
|
|
325
426
|
globalThis.addEventListener("pagehide", onPageHide);
|
|
@@ -340,15 +441,103 @@ function createScrollRestoration(router, options) {
|
|
|
340
441
|
},
|
|
341
442
|
};
|
|
342
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Internal cache-key builder for scroll-position storage.
|
|
446
|
+
*
|
|
447
|
+
* **Exported for testing only — not part of the public API** (intentionally
|
|
448
|
+
* excluded from `index.ts` barrel). Adapter property tests import it via
|
|
449
|
+
* the direct path to lock the `(name, canonicalJson(params))` key shape
|
|
450
|
+
* as a regression guard (§8b H20 / audit-2026-05-16 #S3). A change to
|
|
451
|
+
* key format would silently lose scroll positions across an upgrade —
|
|
452
|
+
* the test set is the contract.
|
|
453
|
+
*
|
|
454
|
+
* ## Identity-based memoization (audit-2026-05-17 §8b #2)
|
|
455
|
+
*
|
|
456
|
+
* `State` objects emitted by core are frozen per-navigation: their
|
|
457
|
+
* `name` / `params` are immutable for the lifetime of the snapshot, and
|
|
458
|
+
* any change produces a new `State` reference. A `WeakMap<State, string>`
|
|
459
|
+
* therefore safely caches the canonicalised key by identity — repeat
|
|
460
|
+
* `keyOf(state)` calls on the same snapshot (typical on
|
|
461
|
+
* back/forward/traverse where the same prior `State` is re-emitted)
|
|
462
|
+
* skip the recursive `canonicalJson` pass entirely.
|
|
463
|
+
*
|
|
464
|
+
* The cache key is the `State` reference, so entries auto-release when
|
|
465
|
+
* the snapshot is GC'd — no eviction needed.
|
|
466
|
+
*/
|
|
467
|
+
const KEY_CACHE = new WeakMap();
|
|
343
468
|
function keyOf(state) {
|
|
344
|
-
|
|
469
|
+
const cached = KEY_CACHE.get(state);
|
|
470
|
+
if (cached !== undefined) {
|
|
471
|
+
return cached;
|
|
472
|
+
}
|
|
473
|
+
const key = `${state.name}:${canonicalJson(state.params)}`;
|
|
474
|
+
KEY_CACHE.set(state, key);
|
|
475
|
+
return key;
|
|
345
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Stable JSON serializer with sorted object keys.
|
|
479
|
+
*
|
|
480
|
+
* **Exported for testing only — not part of the public API** (intentionally
|
|
481
|
+
* excluded from `index.ts` barrel). Adapter property tests import it via
|
|
482
|
+
* the direct path to lock the key-order-insensitive property
|
|
483
|
+
* (`canonicalJson({a:1,b:2}) === canonicalJson({b:2,a:1})`).
|
|
484
|
+
*
|
|
485
|
+
* ## Divergence from `@real-router/sources/canonicalJson` — by design
|
|
486
|
+
*
|
|
487
|
+
* Two independent implementations live in the monorepo:
|
|
488
|
+
*
|
|
489
|
+
* - **`shared/dom-utils/scroll-restore.canonicalJson`** (this file) — scroll
|
|
490
|
+
* cache key builder. Uses `localeCompare` and a plain-object accumulator;
|
|
491
|
+
* tolerates `__proto__`-keyed inputs only insofar as `JSON.stringify`'s
|
|
492
|
+
* replacer happens to sort them; relies on `JSON.stringify`'s native cycle
|
|
493
|
+
* detector. Designed to be cheap on the navigation hot path. The
|
|
494
|
+
* surrounding [[safeKeyOf]] wrapper catches the two crash inputs (`BigInt`,
|
|
495
|
+
* cyclic) and skips the offending capture/restore.
|
|
496
|
+
*
|
|
497
|
+
* - **`@real-router/sources/canonicalJson`** — sources cache key builder.
|
|
498
|
+
* Uses byte-order compare (`< / >`) for locale-independence, a
|
|
499
|
+
* `Object.create(null)` accumulator to prevent prototype pollution, and a
|
|
500
|
+
* bespoke path-based cycle detector (the native one cannot see the cloned
|
|
501
|
+
* graph). Throws eagerly on `Map`/`Set`/`RegExp`/cycles — the caller falls
|
|
502
|
+
* back to a non-cached source.
|
|
503
|
+
*
|
|
504
|
+
* **They are intentionally NOT interchangeable.** Aligning them would either
|
|
505
|
+
* regress scroll-restore performance (byte-order + recursive clone is heavier
|
|
506
|
+
* per call) or weaken the sources cache (locale dependence breaks
|
|
507
|
+
* deterministic cache keys across machines). No cross-package equivalence
|
|
508
|
+
* test exists or should be added; the relationship is "different invariants,
|
|
509
|
+
* different costs, different consumers." Audit-2 / audit-2026-05-17 §2
|
|
510
|
+
* documents the choice.
|
|
511
|
+
*/
|
|
346
512
|
function canonicalJson(value) {
|
|
347
513
|
return JSON.stringify(value, canonicalReplacer);
|
|
348
514
|
}
|
|
349
515
|
function canonicalReplacer(_key, val) {
|
|
516
|
+
// audit-2026-05-17 §5 MEDIUM (Sprint A.3) — function/Symbol marker.
|
|
517
|
+
// `JSON.stringify` silently drops function and symbol values from
|
|
518
|
+
// object output. Two routes that differ ONLY in a function/Symbol
|
|
519
|
+
// value would canonicalize to the same string → silent scroll-cache
|
|
520
|
+
// key collision (positions clobber each other). Replacing the value
|
|
521
|
+
// with a sentinel string breaks the collision while keeping the
|
|
522
|
+
// canonical form deterministic. The sentinels are intentionally
|
|
523
|
+
// ASCII-only and lexically distinct from valid JSON-stringified
|
|
524
|
+
// values; consumers will see `"<fn>"` / `"<sym>"` if they ever
|
|
525
|
+
// round-trip the cache key, signalling the substitution clearly.
|
|
526
|
+
if (typeof val === "function") {
|
|
527
|
+
return "<fn>";
|
|
528
|
+
}
|
|
529
|
+
if (typeof val === "symbol") {
|
|
530
|
+
return "<sym>";
|
|
531
|
+
}
|
|
350
532
|
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
351
|
-
|
|
533
|
+
// Null-prototype accumulator: a plain `{}` would interpret
|
|
534
|
+
// `sorted["__proto__"] = x` as a prototype assignment (silently dropped
|
|
535
|
+
// from JSON.stringify output AND a prototype-pollution vector). Mirrors
|
|
536
|
+
// the same guard in `@real-router/sources/canonicalJson`. The two
|
|
537
|
+
// implementations are still intentionally divergent (see the doc-block
|
|
538
|
+
// on [[canonicalJson]] above), but prototype-safety is non-negotiable
|
|
539
|
+
// on both. Lock-test: scrollRestoreKey.properties.ts Invariant 11.
|
|
540
|
+
const sorted = Object.create(null);
|
|
352
541
|
// eslint-disable-next-line unicorn/no-array-sort -- ng-packagr uses pre-ES2023 lib; toSorted unavailable
|
|
353
542
|
const keys = Object.keys(val).sort((left, right) => left.localeCompare(right));
|
|
354
543
|
for (const key of keys) {
|
|
@@ -485,14 +674,39 @@ function shouldNavigate(evt) {
|
|
|
485
674
|
!evt.ctrlKey &&
|
|
486
675
|
!evt.shiftKey);
|
|
487
676
|
}
|
|
677
|
+
// Matches a single percent-escape triple (`%` + two hex digits). Used as
|
|
678
|
+
// the "already-encoded" probe in `encodeFragmentInline` below — see the
|
|
679
|
+
// idempotency rationale there.
|
|
680
|
+
const PERCENT_ESCAPE_PROBE = /%[\dA-Fa-f]{2}/;
|
|
488
681
|
/**
|
|
489
682
|
* RFC 3986 fragment encoding: preserve sub-delims (`&`, `=`, `?`, `:`),
|
|
490
683
|
* encode space, `%`, control chars, non-ASCII via encodeURI; defensively
|
|
491
684
|
* escape `#` (encodeURI does not). Mirrors `encodeHashFragment` in
|
|
492
685
|
* `shared/browser-env/url-context.ts` — duplicated here because the
|
|
493
686
|
* shared/dom-utils symlink graph does not reach shared/browser-env.
|
|
687
|
+
*
|
|
688
|
+
* **Idempotency for pre-encoded input (audit-2026-05-17 §5 MEDIUM E.1).**
|
|
689
|
+
* The doc-comment on `<Link hash>` says the value is a "decoded fragment
|
|
690
|
+
* without leading #". But realistic consumers copy hashes out of
|
|
691
|
+
* `location.hash` (which is percent-encoded) and pass them back, so the
|
|
692
|
+
* naive `encodeURI("%20")` would double-encode into `"%2520"` and break
|
|
693
|
+
* anchor lookup. We detect a percent-escape triple in the input and, if
|
|
694
|
+
* present, decode + re-encode for idempotency. Malformed `%XX` (e.g.
|
|
695
|
+
* `"%2"` or `"%ZZ"`) makes `decodeURIComponent` throw — in that case we
|
|
696
|
+
* fall through to plain `encodeURI`, which never throws.
|
|
494
697
|
*/
|
|
495
698
|
function encodeFragmentInline(decoded) {
|
|
699
|
+
if (PERCENT_ESCAPE_PROBE.test(decoded)) {
|
|
700
|
+
try {
|
|
701
|
+
const roundtrip = decodeURIComponent(decoded);
|
|
702
|
+
return encodeURI(roundtrip).replaceAll("#", "%23");
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// Malformed `%XX` — fall through to the plain encoding path.
|
|
706
|
+
// encodeURI does not throw on malformed escapes; it treats the
|
|
707
|
+
// `%` as a literal and percent-encodes it (`%2` → `%252`).
|
|
708
|
+
}
|
|
709
|
+
}
|
|
496
710
|
return encodeURI(decoded).replaceAll("#", "%23");
|
|
497
711
|
}
|
|
498
712
|
/**
|
|
@@ -518,11 +732,28 @@ function buildHref(router, routeName, routeParams, options) {
|
|
|
518
732
|
const buildUrl = router.buildUrl;
|
|
519
733
|
if (buildUrl) {
|
|
520
734
|
const url = buildUrl(routeName, routeParams, normHash === undefined ? undefined : { hash: normHash });
|
|
521
|
-
|
|
735
|
+
// Accept only non-empty strings. The BuildUrlFn type contract is
|
|
736
|
+
// `string | undefined`, but defensive against:
|
|
737
|
+
// - `""` (empty string) → would render `<a href="">`, which resolves
|
|
738
|
+
// to the current page URL → silent self-navigation on click.
|
|
739
|
+
// - `null` (type-contract violation) → would render `<a href={null}>`,
|
|
740
|
+
// stringified to `"null"` in some renderers.
|
|
741
|
+
// Either case falls through to the `router.buildPath` fallback below.
|
|
742
|
+
if (typeof url === "string" && url.length > 0) {
|
|
522
743
|
return url;
|
|
523
744
|
}
|
|
524
745
|
}
|
|
525
746
|
const path = router.buildPath(routeName, routeParams);
|
|
747
|
+
// Symmetric to the buildUrl guard above (#S1 audit, Invariant 12).
|
|
748
|
+
// `router.buildPath` is typed `string`, but defends against:
|
|
749
|
+
// - `""` (empty string) — would render `<a href="">`, which resolves
|
|
750
|
+
// to the current page URL → silent self-navigation on click.
|
|
751
|
+
// - non-string type-contract violations from custom path-matchers.
|
|
752
|
+
// Both yield `undefined` (renderer drops the attribute) with a warning.
|
|
753
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
754
|
+
console.error(`[real-router] Route "${routeName}" yielded an empty path. The element will render without an href attribute.`);
|
|
755
|
+
return undefined;
|
|
756
|
+
}
|
|
526
757
|
return normHash ? `${path}#${encodeFragmentInline(normHash)}` : path;
|
|
527
758
|
}
|
|
528
759
|
catch {
|
|
@@ -548,8 +779,25 @@ function navigateWithHash(router, routeName, routeParams, hash, extraOptions) {
|
|
|
548
779
|
}
|
|
549
780
|
return router.navigate(routeName, routeParams, opts);
|
|
550
781
|
}
|
|
782
|
+
// Match-any-whitespace regex shared across calls. RegExp literals at
|
|
783
|
+
// call-site recompile in some engines; lifting it avoids that microcost
|
|
784
|
+
// for the slow-path branch.
|
|
785
|
+
const WHITESPACE_PROBE = /\s/;
|
|
786
|
+
const WHITESPACE_SPLIT = /\S+/g;
|
|
551
787
|
function parseTokens(value) {
|
|
552
|
-
|
|
788
|
+
if (!value) {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
// Hot-path fast-path (audit-2026-05-17 §8b #1): >99% of active-class
|
|
792
|
+
// inputs at `<Link>` emit are single-token strings like `"active"` or
|
|
793
|
+
// `"is-current"` — no whitespace, no leading/trailing pad. Skip the
|
|
794
|
+
// regex match and Array result allocation: a literal `[value]` works
|
|
795
|
+
// because the slow-path `match(/\S+/g)` would return exactly `[value]`
|
|
796
|
+
// for the same input. PBT lock: linkUtils.properties.ts Invariant 13.
|
|
797
|
+
if (!WHITESPACE_PROBE.test(value)) {
|
|
798
|
+
return [value];
|
|
799
|
+
}
|
|
800
|
+
return value.match(WHITESPACE_SPLIT) ?? [];
|
|
553
801
|
}
|
|
554
802
|
function buildActiveClassName(isActive, activeClassName, baseClassName) {
|
|
555
803
|
if (isActive && activeClassName) {
|
|
@@ -572,6 +820,29 @@ function buildActiveClassName(isActive, activeClassName, baseClassName) {
|
|
|
572
820
|
}
|
|
573
821
|
return baseClassName ?? undefined;
|
|
574
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* One-level structural equality using `Object.is` per key.
|
|
825
|
+
*
|
|
826
|
+
* **String-keyed properties only (Mini-sprint E.3 — audit-5 §4.2 #3).**
|
|
827
|
+
* Implementation walks `Object.keys()` which by spec returns only
|
|
828
|
+
* enumerable own STRING keys. Symbol-keyed properties — created via
|
|
829
|
+
* `obj[Symbol("brand")] = value` or `{ [Symbol(...)]: value }` — are
|
|
830
|
+
* NOT compared. Two records that differ only in a Symbol-keyed value
|
|
831
|
+
* will compare as equal.
|
|
832
|
+
*
|
|
833
|
+
* This is intentional: route params and Link options are documented as
|
|
834
|
+
* string-keyed primitives (string | number | boolean) — Symbol-keyed
|
|
835
|
+
* metadata (e.g. brand markers, private state) doesn't belong in a
|
|
836
|
+
* cache-key comparison. Switching to `Reflect.ownKeys()` would extend
|
|
837
|
+
* the contract to symbols at the cost of one extra allocation per call
|
|
838
|
+
* (Reflect.ownKeys composes string-keys + symbol-keys arrays). If a
|
|
839
|
+
* consumer relies on symbol-keyed metadata for navigation
|
|
840
|
+
* disambiguation, they should encode it into a string key instead.
|
|
841
|
+
*
|
|
842
|
+
* Mirrors React's `shallowEqual` (packages/shared/shallowEqual.js) in
|
|
843
|
+
* both the string-keys-only semantics and the `hasOwnProperty` guard
|
|
844
|
+
* below.
|
|
845
|
+
*/
|
|
575
846
|
function shallowEqual(prev, next) {
|
|
576
847
|
if (Object.is(prev, next)) {
|
|
577
848
|
return true;
|
|
@@ -586,7 +857,11 @@ function shallowEqual(prev, next) {
|
|
|
586
857
|
const prevRecord = prev;
|
|
587
858
|
const nextRecord = next;
|
|
588
859
|
for (const key of prevKeys) {
|
|
589
|
-
|
|
860
|
+
// hasOwnProperty guard: without it, a key missing in `next` reads as
|
|
861
|
+
// `undefined` and falsely matches `prev[key] === undefined`. Same shape
|
|
862
|
+
// as React's shallowEqual (packages/shared/shallowEqual.js).
|
|
863
|
+
if (!Object.prototype.hasOwnProperty.call(next, key) ||
|
|
864
|
+
!Object.is(prevRecord[key], nextRecord[key])) {
|
|
590
865
|
return false;
|
|
591
866
|
}
|
|
592
867
|
}
|
|
@@ -596,8 +871,23 @@ function applyLinkA11y(element) {
|
|
|
596
871
|
if (!element) {
|
|
597
872
|
return;
|
|
598
873
|
}
|
|
599
|
-
|
|
600
|
-
|
|
874
|
+
// Cross-realm safety (audit-2026-05-17 §5 HIGH #4):
|
|
875
|
+
// `instanceof HTMLAnchorElement` compares against the constructor from
|
|
876
|
+
// the CURRENT realm. An element created in a different window (iframe
|
|
877
|
+
// contentDocument, micro-frontend, embedded widget) fails the check
|
|
878
|
+
// even when it IS a real anchor — the helper would then inject
|
|
879
|
+
// role="link" + tabindex="0" on top of native anchor semantics,
|
|
880
|
+
// breaking screen reader output ("link link") and focus order.
|
|
881
|
+
//
|
|
882
|
+
// tagName is realm-agnostic and is uppercase for HTML-namespaced
|
|
883
|
+
// elements in any document. SVG `<a>` has lowercase tagName plus a
|
|
884
|
+
// different prototype (SVGAElement) — skipping it here is wrong by
|
|
885
|
+
// accident: SVG anchors don't have keyboard activation semantics the
|
|
886
|
+
// helper would add. But they also don't reach this helper in
|
|
887
|
+
// practice (router Link components emit HTML anchors). Lock the
|
|
888
|
+
// uppercase compare to keep the contract narrow.
|
|
889
|
+
const tag = element.tagName;
|
|
890
|
+
if (tag === "A" || tag === "BUTTON") {
|
|
601
891
|
return;
|
|
602
892
|
}
|
|
603
893
|
if (!element.hasAttribute("role")) {
|
|
@@ -608,6 +898,65 @@ function applyLinkA11y(element) {
|
|
|
608
898
|
}
|
|
609
899
|
}
|
|
610
900
|
|
|
901
|
+
/**
|
|
902
|
+
* Shared installation helpers for `provideRealRouter` and
|
|
903
|
+
* `provideRealRouterFactory`. Must be called inside the body of a
|
|
904
|
+
* `provideEnvironmentInitializer(() => { ... })` callback so the active
|
|
905
|
+
* injection context resolves `ROUTER`, `ApplicationRef`, and `DestroyRef`.
|
|
906
|
+
*
|
|
907
|
+
* Closes review-2026-05-10 §8.1 MED — eliminates duplicate wiring between
|
|
908
|
+
* `providers.ts` and `providersFactory.ts` (high drift risk noted in the
|
|
909
|
+
* audit: the comment blocks were identical down to the punctuation).
|
|
910
|
+
*/
|
|
911
|
+
function installScrollRestoration(options) {
|
|
912
|
+
const router = inject(ROUTER);
|
|
913
|
+
const sr = createScrollRestoration(router, options);
|
|
914
|
+
inject(DestroyRef).onDestroy(() => {
|
|
915
|
+
sr.destroy();
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
function installViewTransitions() {
|
|
919
|
+
const router = inject(ROUTER);
|
|
920
|
+
// Feature-detect `document.startViewTransition` once at install time. The
|
|
921
|
+
// `appRef.tick()` listener exists ONLY to feed Angular's zoneless CD into
|
|
922
|
+
// the VT utility's `setTimeout(0)`-driven snapshot capture (see comment
|
|
923
|
+
// below). When `startViewTransition` is unavailable (Firefox as of 2026-04,
|
|
924
|
+
// SSR, older browsers), `createViewTransitions` short-circuits to its
|
|
925
|
+
// frozen NOOP_INSTANCE — no leave subscriber registered, no
|
|
926
|
+
// `setTimeout(0)` invariant to satisfy. Installing the per-navigation
|
|
927
|
+
// tick listener anyway would force a synchronous CD pass on every
|
|
928
|
+
// navigation with zero benefit, doubling CD work in zoneless apps.
|
|
929
|
+
// Closes review-2026-05-10 §8.2 MED (view-transitions hot path).
|
|
930
|
+
const vtAvailable = typeof document !== "undefined" &&
|
|
931
|
+
typeof document.startViewTransition === "function";
|
|
932
|
+
let offTick;
|
|
933
|
+
if (vtAvailable) {
|
|
934
|
+
// Force synchronous change detection on every transition success BEFORE
|
|
935
|
+
// the VT utility resolves its deferred. The utility uses `setTimeout(0)`
|
|
936
|
+
// to release the new-snapshot capture, which is load-bearing because
|
|
937
|
+
// Chromium blocks rAF callbacks while VT sits in the
|
|
938
|
+
// `update-callback-called` phase. Angular's zoneless CD is rAF-driven by
|
|
939
|
+
// default — without this synchronous tick the new DOM is not committed
|
|
940
|
+
// when the browser captures the new snapshot, so old and new snapshots
|
|
941
|
+
// end up identical and animations finish in ~0 ms with no visible work
|
|
942
|
+
// (the inner-route `products.list ↔ products.detail` morph in the
|
|
943
|
+
// example app was the canary).
|
|
944
|
+
//
|
|
945
|
+
// Subscribers fire in registration order; this one runs BEFORE
|
|
946
|
+
// `createViewTransitions` registers its own subscriber, guaranteeing CD
|
|
947
|
+
// completes first.
|
|
948
|
+
const appRef = inject(ApplicationRef);
|
|
949
|
+
offTick = router.subscribe(() => {
|
|
950
|
+
appRef.tick();
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
const vt = createViewTransitions(router);
|
|
954
|
+
inject(DestroyRef).onDestroy(() => {
|
|
955
|
+
offTick?.();
|
|
956
|
+
vt.destroy();
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
611
960
|
/** Must be called within an injection context (constructor, field initializer, runInInjectionContext). */
|
|
612
961
|
function sourceToSignal(source) {
|
|
613
962
|
const sig = signal(source.getSnapshot(), ...(ngDevMode ? [{ debugName: "sig" }] : /* istanbul ignore next */ []));
|
|
@@ -616,8 +965,17 @@ function sourceToSignal(source) {
|
|
|
616
965
|
sig.set(source.getSnapshot());
|
|
617
966
|
});
|
|
618
967
|
destroyRef.onDestroy(() => {
|
|
619
|
-
|
|
620
|
-
|
|
968
|
+
// `try/finally` guarantees `source.destroy()` runs even if `unsubscribe`
|
|
969
|
+
// throws. Cached sources from `@real-router/sources` keep `destroy()` as
|
|
970
|
+
// a no-op (so they survive multi-consumer teardown), but non-cached
|
|
971
|
+
// sources rely on this call to release their router subscription —
|
|
972
|
+
// skipping it on an unsubscribe throw would leak the listener.
|
|
973
|
+
try {
|
|
974
|
+
unsubscribe();
|
|
975
|
+
}
|
|
976
|
+
finally {
|
|
977
|
+
source.destroy();
|
|
978
|
+
}
|
|
621
979
|
});
|
|
622
980
|
return sig.asReadonly();
|
|
623
981
|
}
|
|
@@ -627,6 +985,11 @@ const NAVIGATOR = new InjectionToken("NAVIGATOR");
|
|
|
627
985
|
const ROUTE = new InjectionToken("ROUTE");
|
|
628
986
|
function provideRealRouter(router, options) {
|
|
629
987
|
const navigator = getNavigator(router);
|
|
988
|
+
// `Parameters<typeof makeEnvironmentProviders>[0]` is the actual union
|
|
989
|
+
// `(Provider | EnvironmentProviders | EnvironmentProviders[])[]` —
|
|
990
|
+
// `provideEnvironmentInitializer()` returns `EnvironmentProviders`, so the
|
|
991
|
+
// narrower `Provider[]` would force a cast at every push (review §8a — the
|
|
992
|
+
// proposed Provider[] swap was retracted after discovering this).
|
|
630
993
|
const providers = [
|
|
631
994
|
{ provide: ROUTER, useValue: router },
|
|
632
995
|
{ provide: NAVIGATOR, useValue: navigator },
|
|
@@ -641,66 +1004,213 @@ function provideRealRouter(router, options) {
|
|
|
641
1004
|
if (options?.scrollRestoration) {
|
|
642
1005
|
const scrollOpts = options.scrollRestoration;
|
|
643
1006
|
providers.push(provideEnvironmentInitializer(() => {
|
|
644
|
-
|
|
645
|
-
inject(DestroyRef).onDestroy(() => {
|
|
646
|
-
sr.destroy();
|
|
647
|
-
});
|
|
1007
|
+
installScrollRestoration(scrollOpts);
|
|
648
1008
|
}));
|
|
649
1009
|
}
|
|
650
1010
|
if (options?.viewTransitions === true) {
|
|
1011
|
+
providers.push(provideEnvironmentInitializer(installViewTransitions));
|
|
1012
|
+
}
|
|
1013
|
+
return makeEnvironmentProviders(providers);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* `TransferState` key carrying the SSR-resolved router state from server to
|
|
1018
|
+
* client as an XSS-safe JSON string (produced by `serializeRouterState`).
|
|
1019
|
+
* Populated server-side by the `provideAppInitializer` callback after
|
|
1020
|
+
* `router.start()` resolves; consumed client-side after hydration. Mirrors the
|
|
1021
|
+
* `<script>window.__SSR_STATE__ = …</script>` pattern used by every other
|
|
1022
|
+
* adapter — Angular's idiomatic transport is `TransferState` (#599).
|
|
1023
|
+
*
|
|
1024
|
+
* Stored as `string`: `serializeRouterState(state)` already produces JSON;
|
|
1025
|
+
* `hydrateRouter(router, json)` accepts a JSON string and parses it once
|
|
1026
|
+
* internally. Storing the parsed object would force a double round-trip
|
|
1027
|
+
* (TransferState wraps every value in JSON for transport).
|
|
1028
|
+
*
|
|
1029
|
+
* Internal implementation detail. Not re-exported.
|
|
1030
|
+
*/
|
|
1031
|
+
const ROUTER_STATE_KEY = makeStateKey("@real-router/angular:ssrState");
|
|
1032
|
+
/**
|
|
1033
|
+
* `provideRealRouterFactory` — environment providers for SSR / SSG scenarios.
|
|
1034
|
+
*
|
|
1035
|
+
* Unlike `provideRealRouter(router)` (single instance via `useValue`), this
|
|
1036
|
+
* factory uses `useFactory` to produce a per-request router clone:
|
|
1037
|
+
*
|
|
1038
|
+
* 1. Reads Angular's `REQUEST` token (`{ optional: true }`).
|
|
1039
|
+
* 2. Calls `cloneRouter(baseRouter, deps?.(request))` to create a request-scoped clone.
|
|
1040
|
+
* 3. Applies plugins (`plugins` array or `plugins(request)` factory).
|
|
1041
|
+
* 4. Registers `provideAppInitializer` that calls `await router.start(url)`.
|
|
1042
|
+
* 5. Schedules `router.dispose()` via `DestroyRef.onDestroy` — the request
|
|
1043
|
+
* Injector is destroyed after the response is sent, releasing all
|
|
1044
|
+
* subscriptions and plugins.
|
|
1045
|
+
*
|
|
1046
|
+
* Use cases:
|
|
1047
|
+
* - Angular SSR with `@angular/ssr` (`outputMode: "server"`).
|
|
1048
|
+
* - SSG build-time render via `renderApplication` + `platformProviders` `REQUEST` mock.
|
|
1049
|
+
* - Multi-tenant request-scoped routing.
|
|
1050
|
+
*
|
|
1051
|
+
* Existing single-instance scenarios (SPA, SSG client after hydration) continue
|
|
1052
|
+
* to use `provideRealRouter(router)` — both APIs ship in parallel.
|
|
1053
|
+
*
|
|
1054
|
+
* @param options - Factory configuration — see `RealRouterFactoryOptions`.
|
|
1055
|
+
* @returns `EnvironmentProviders` to spread into `ApplicationConfig.providers`.
|
|
1056
|
+
*/
|
|
1057
|
+
function provideRealRouterFactory(options) {
|
|
1058
|
+
const { baseRouter, plugins, deps, scrollRestoration, viewTransitions } = options;
|
|
1059
|
+
const providers = [
|
|
1060
|
+
{
|
|
1061
|
+
provide: ROUTER,
|
|
1062
|
+
useFactory: () => {
|
|
1063
|
+
const request = inject(REQUEST, { optional: true });
|
|
1064
|
+
const requestDeps = deps?.(request);
|
|
1065
|
+
const router = cloneRouter(baseRouter, requestDeps);
|
|
1066
|
+
const pluginList = typeof plugins === "function" ? plugins(request) : plugins;
|
|
1067
|
+
if (pluginList && pluginList.length > 0) {
|
|
1068
|
+
// Variadic — `usePlugin` accepts `(PluginFactory<D> | false | null | undefined)[]`.
|
|
1069
|
+
router.usePlugin(...pluginList);
|
|
1070
|
+
}
|
|
1071
|
+
// Per-request cleanup. The application Injector is destroyed:
|
|
1072
|
+
// - On server: after `writeResponseToNodeResponse` finishes the response
|
|
1073
|
+
// (request scope ends).
|
|
1074
|
+
// - On client: at `ApplicationRef.destroy` (rare in SPA, common in TestBed).
|
|
1075
|
+
// - In SSG build: after each `renderApplication` resolves.
|
|
1076
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1077
|
+
router.dispose();
|
|
1078
|
+
});
|
|
1079
|
+
return router;
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
provide: NAVIGATOR,
|
|
1084
|
+
useFactory: () => getNavigator(inject(ROUTER)),
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
provide: ROUTE,
|
|
1088
|
+
useFactory: () => {
|
|
1089
|
+
const router = inject(ROUTER);
|
|
1090
|
+
return {
|
|
1091
|
+
routeState: sourceToSignal(createRouteSource(router)),
|
|
1092
|
+
navigator: inject(NAVIGATOR),
|
|
1093
|
+
};
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
// Async bootstrap — runs before the first component renders. Three
|
|
1097
|
+
// branches based on TransferState population:
|
|
1098
|
+
//
|
|
1099
|
+
// 1. **Client after hydration** — server populated TransferState with
|
|
1100
|
+
// the SSR-resolved router state. Consume it via `hydrateRouter`,
|
|
1101
|
+
// which deposits the parsed state into the one-shot
|
|
1102
|
+
// `RouterInternals.hydrationState` scratchpad before invoking
|
|
1103
|
+
// `router.start(state.path)`. SSR loader plugins
|
|
1104
|
+
// (`@real-router/ssr-data-plugin`, `@real-router/rsc-server-plugin`)
|
|
1105
|
+
// read the scratchpad and skip the loader on first paint — parity
|
|
1106
|
+
// with the other 5 adapters that consume `<script>__SSR_STATE__</script>` (#596, #599).
|
|
1107
|
+
//
|
|
1108
|
+
// 2. **Server / SSG** — TransferState empty; run the regular
|
|
1109
|
+
// `router.start(path)`. After it resolves, write the serialized
|
|
1110
|
+
// state back into TransferState so the matching client run lands
|
|
1111
|
+
// in branch 1. Angular's `TransferState` infrastructure
|
|
1112
|
+
// (provided by `provideClientHydration()`) carries this blob to
|
|
1113
|
+
// the client as a `<script id="ng-state">` payload.
|
|
1114
|
+
//
|
|
1115
|
+
// 3. **Pure CSR** — TransferState empty (never populated by a server
|
|
1116
|
+
// pass), and `inject(REQUEST, { optional: true })` returns null.
|
|
1117
|
+
// Falls into the same `router.start(path)` branch as server-side
|
|
1118
|
+
// but skips the TransferState write (no client to hand off to).
|
|
1119
|
+
//
|
|
1120
|
+
// Errors propagate (Option A from RFC §10): the bootstrap fails and the
|
|
1121
|
+
// server returns 500. Custom error pages should be wired via
|
|
1122
|
+
// `RouterErrorBoundary` on subsequent renders.
|
|
1123
|
+
provideAppInitializer(async () => {
|
|
1124
|
+
const router = inject(ROUTER);
|
|
1125
|
+
const request = inject(REQUEST, { optional: true });
|
|
1126
|
+
const transferState = inject(TransferState);
|
|
1127
|
+
const ssrJson = transferState.get(ROUTER_STATE_KEY, null);
|
|
1128
|
+
if (ssrJson !== null) {
|
|
1129
|
+
// Branch 1: client after hydration — reuse server-resolved state.
|
|
1130
|
+
await hydrateRouter(router, ssrJson);
|
|
1131
|
+
// One-shot semantic, parity with `delete window.__SSR_STATE__`.
|
|
1132
|
+
transferState.remove(ROUTER_STATE_KEY);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
// Branches 2 & 3: regular start.
|
|
1136
|
+
// Browser-plugin's `start` interceptor (when registered) wraps this call
|
|
1137
|
+
// with location-derived path. We always pass an explicit string — the
|
|
1138
|
+
// interceptor uses the explicit value because `next(path ?? location)`
|
|
1139
|
+
// short-circuits when `path` is non-nullish.
|
|
1140
|
+
const path = deriveStartPath(request);
|
|
1141
|
+
const state = await router.start(path);
|
|
1142
|
+
if (request !== null) {
|
|
1143
|
+
// Branch 2: running inside `@angular/ssr`'s request handler — write
|
|
1144
|
+
// serialized state to TransferState so the matching client run can
|
|
1145
|
+
// skip the loader on first paint.
|
|
1146
|
+
transferState.set(ROUTER_STATE_KEY, serializeRouterState(state));
|
|
1147
|
+
}
|
|
1148
|
+
}),
|
|
1149
|
+
];
|
|
1150
|
+
if (scrollRestoration) {
|
|
651
1151
|
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
|
-
});
|
|
1152
|
+
installScrollRestoration(scrollRestoration);
|
|
674
1153
|
}));
|
|
675
1154
|
}
|
|
1155
|
+
if (viewTransitions === true) {
|
|
1156
|
+
providers.push(provideEnvironmentInitializer(installViewTransitions));
|
|
1157
|
+
}
|
|
676
1158
|
return makeEnvironmentProviders(providers);
|
|
677
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Derive the path passed to `router.start(path)`:
|
|
1162
|
+
* - Server / SSG: `request.url` → pathname + search.
|
|
1163
|
+
* - Client: `window.location` if available.
|
|
1164
|
+
* - Fallback: `"/"` (only reachable in synthetic non-browser non-SSR setups).
|
|
1165
|
+
*/
|
|
1166
|
+
function deriveStartPath(request) {
|
|
1167
|
+
if (request) {
|
|
1168
|
+
const url = new URL(request.url);
|
|
1169
|
+
return url.pathname + url.search;
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof globalThis.window !== "undefined") {
|
|
1172
|
+
return globalThis.location.pathname + globalThis.location.search;
|
|
1173
|
+
}
|
|
1174
|
+
return "/";
|
|
1175
|
+
}
|
|
678
1176
|
|
|
679
1177
|
function injectOrThrow(token, fnName) {
|
|
680
1178
|
const value = inject(token, { optional: true });
|
|
681
|
-
|
|
1179
|
+
// Explicit null / undefined check — falsy guard would misfire on
|
|
1180
|
+
// legitimately falsy values (`0`, `""`, `false`) if the token were ever
|
|
1181
|
+
// typed for primitives. Today all our tokens hold object instances, but
|
|
1182
|
+
// pinning the check keeps the function safe for future typing changes.
|
|
1183
|
+
if (value === null || value === undefined) {
|
|
682
1184
|
throw new Error(`${fnName} must be used within a provideRealRouter context`);
|
|
683
1185
|
}
|
|
684
1186
|
return value;
|
|
685
1187
|
}
|
|
686
1188
|
|
|
687
1189
|
function injectRouter() {
|
|
1190
|
+
assertInInjectionContext(injectRouter);
|
|
688
1191
|
return injectOrThrow(ROUTER, "injectRouter");
|
|
689
1192
|
}
|
|
690
1193
|
|
|
691
1194
|
function injectNavigator() {
|
|
1195
|
+
assertInInjectionContext(injectNavigator);
|
|
692
1196
|
return injectOrThrow(NAVIGATOR, "injectNavigator");
|
|
693
1197
|
}
|
|
694
1198
|
|
|
695
1199
|
function injectRoute() {
|
|
1200
|
+
assertInInjectionContext(injectRoute);
|
|
696
1201
|
const signals = injectOrThrow(ROUTE, "injectRoute");
|
|
697
|
-
|
|
1202
|
+
// Read the snapshot once: the signal is reactive, but the throw-guard
|
|
1203
|
+
// and any future use of the snapshot within this call should observe the
|
|
1204
|
+
// SAME value to avoid races.
|
|
1205
|
+
const snapshot = signals.routeState();
|
|
1206
|
+
if (!snapshot.route) {
|
|
698
1207
|
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
1208
|
}
|
|
700
1209
|
return signals;
|
|
701
1210
|
}
|
|
702
1211
|
|
|
703
1212
|
function injectRouteNode(nodeName) {
|
|
1213
|
+
assertInInjectionContext(injectRouteNode);
|
|
704
1214
|
const router = injectRouter();
|
|
705
1215
|
const navigator = getNavigator(router);
|
|
706
1216
|
const source = createRouteNodeSource(router, nodeName);
|
|
@@ -709,26 +1219,37 @@ function injectRouteNode(nodeName) {
|
|
|
709
1219
|
}
|
|
710
1220
|
|
|
711
1221
|
function injectRouteUtils() {
|
|
1222
|
+
assertInInjectionContext(injectRouteUtils);
|
|
712
1223
|
const router = injectRouter();
|
|
713
1224
|
return getRouteUtils(getPluginApi(router).getTree());
|
|
714
1225
|
}
|
|
715
1226
|
|
|
716
1227
|
function injectRouterTransition() {
|
|
1228
|
+
assertInInjectionContext(injectRouterTransition);
|
|
717
1229
|
const router = injectRouter();
|
|
718
1230
|
const source = getTransitionSource(router);
|
|
719
1231
|
return sourceToSignal(source);
|
|
720
1232
|
}
|
|
721
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* Build the `options` literal for `createActiveRouteSource` while honoring
|
|
1236
|
+
* `exactOptionalPropertyTypes` — the type forbids passing `{ hash: undefined }`
|
|
1237
|
+
* literally (#532), so callers must conditionally include the `hash` key only
|
|
1238
|
+
* when a value was provided.
|
|
1239
|
+
*
|
|
1240
|
+
* Used by `RealLink`, `RealLinkActive`, and `injectIsActiveRoute` — extracted
|
|
1241
|
+
* from three identical ternaries (review-2026-05-16 §8a LOW).
|
|
1242
|
+
*/
|
|
1243
|
+
function buildActiveRouteOptions(strict, ignoreQueryParams, hash) {
|
|
1244
|
+
return hash === undefined
|
|
1245
|
+
? { strict, ignoreQueryParams }
|
|
1246
|
+
: { strict, ignoreQueryParams, hash };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
722
1249
|
function injectIsActiveRoute(routeName, params, options) {
|
|
1250
|
+
assertInInjectionContext(injectIsActiveRoute);
|
|
723
1251
|
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 });
|
|
1252
|
+
const source = createActiveRouteSource(router, routeName, params, buildActiveRouteOptions(options?.strict ?? false, options?.ignoreQueryParams ?? true, options?.hash));
|
|
732
1253
|
return sourceToSignal(source);
|
|
733
1254
|
}
|
|
734
1255
|
|
|
@@ -870,7 +1391,6 @@ function injectRouteEnter(handler, options) {
|
|
|
870
1391
|
assertInInjectionContext(injectRouteEnter);
|
|
871
1392
|
const { routeState } = injectRoute();
|
|
872
1393
|
const skipSameRoute = options?.skipSameRoute ?? true;
|
|
873
|
-
let lastHandledRoute = null;
|
|
874
1394
|
effect(() => {
|
|
875
1395
|
const { route, previousRoute } = routeState();
|
|
876
1396
|
// Early-exit guards, top-down:
|
|
@@ -880,23 +1400,19 @@ function injectRouteEnter(handler, options) {
|
|
|
880
1400
|
// - **Skip-same-route**: query-only navigations have
|
|
881
1401
|
// `transition.from === route.name`. Opt-out via
|
|
882
1402
|
// `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
1403
|
if (!route.transition.from) {
|
|
889
1404
|
return;
|
|
890
1405
|
}
|
|
891
1406
|
if (skipSameRoute && route.transition.from === route.name) {
|
|
892
1407
|
return;
|
|
893
1408
|
}
|
|
894
|
-
|
|
895
|
-
|
|
1409
|
+
// `previousRoute` is guaranteed populated whenever `route.transition.from`
|
|
1410
|
+
// is set — core writes them together. The dead-code throw-guard that used
|
|
1411
|
+
// to live here (review §8a LOW) is removed; the narrowing below is the
|
|
1412
|
+
// type-safe equivalent and avoids the no-non-null-assertion lint.
|
|
1413
|
+
if (!previousRoute) {
|
|
896
1414
|
return;
|
|
897
1415
|
}
|
|
898
|
-
/* v8 ignore stop */
|
|
899
|
-
lastHandledRoute = route;
|
|
900
1416
|
handler({ route, previousRoute });
|
|
901
1417
|
});
|
|
902
1418
|
}
|
|
@@ -904,60 +1420,131 @@ function injectRouteEnter(handler, options) {
|
|
|
904
1420
|
class RouteMatch {
|
|
905
1421
|
routeMatch = input.required(...(ngDevMode ? [{ debugName: "routeMatch" }] : /* istanbul ignore next */ []));
|
|
906
1422
|
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.
|
|
1423
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteMatch, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1424
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", type: RouteMatch, isStandalone: true, selector: "ng-template[routeMatch]", inputs: { routeMatch: { classPropertyName: "routeMatch", publicName: "routeMatch", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
909
1425
|
}
|
|
910
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1426
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteMatch, decorators: [{
|
|
911
1427
|
type: Directive,
|
|
912
1428
|
args: [{ selector: "ng-template[routeMatch]" }]
|
|
913
1429
|
}], propDecorators: { routeMatch: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeMatch", required: true }] }] } });
|
|
914
1430
|
|
|
915
1431
|
class RouteNotFound {
|
|
916
1432
|
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.
|
|
1433
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteNotFound, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1434
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.13", type: RouteNotFound, isStandalone: true, selector: "ng-template[routeNotFound]", ngImport: i0 });
|
|
919
1435
|
}
|
|
920
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1436
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteNotFound, decorators: [{
|
|
921
1437
|
type: Directive,
|
|
922
1438
|
args: [{ selector: "ng-template[routeNotFound]" }]
|
|
923
1439
|
}] });
|
|
924
1440
|
|
|
925
1441
|
class RouteSelf {
|
|
926
1442
|
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.
|
|
1443
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteSelf, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1444
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.13", type: RouteSelf, isStandalone: true, selector: "ng-template[routeSelf]", ngImport: i0 });
|
|
929
1445
|
}
|
|
930
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1446
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteSelf, decorators: [{
|
|
931
1447
|
type: Directive,
|
|
932
1448
|
args: [{ selector: "ng-template[routeSelf]" }]
|
|
933
1449
|
}] });
|
|
934
1450
|
|
|
935
|
-
|
|
1451
|
+
/**
|
|
1452
|
+
* Subscribe a `RouterSource<T>` to a write-callback and return a cleanup
|
|
1453
|
+
* function. The shape is the per-effect-run pattern that `RealLink`,
|
|
1454
|
+
* `RealLinkActive`, and `RouteView` all share inside their constructor
|
|
1455
|
+
* `effect(...)` (review-2026-05-16 §8a MEDIUM — identical 8-line block
|
|
1456
|
+
* repeated in 3 directives):
|
|
1457
|
+
*
|
|
1458
|
+
* 1. Read initial snapshot and apply it via `onSnapshot(snap)`.
|
|
1459
|
+
* 2. Subscribe — every subsequent emission calls `onSnapshot(snap)` again.
|
|
1460
|
+
* 3. Return a cleanup that unsubscribes and destroys the source. For
|
|
1461
|
+
* cached factories from `@real-router/sources` (`createActiveRouteSource`,
|
|
1462
|
+
* `createRouteNodeSource`, `getTransitionSource`, `getErrorSource`,
|
|
1463
|
+
* `createDismissableError`) `destroy()` is a no-op on the shared
|
|
1464
|
+
* wrapper, so this helper is safe to invoke from rapid effect re-runs
|
|
1465
|
+
* under signal-input changes.
|
|
1466
|
+
*
|
|
1467
|
+
* Callers pass the result to `onCleanup(...)` from Angular's `effect()`.
|
|
1468
|
+
*
|
|
1469
|
+
* @example
|
|
1470
|
+
* ```ts
|
|
1471
|
+
* effect((onCleanup) => {
|
|
1472
|
+
* const source = createActiveRouteSource(router, routeName(), params());
|
|
1473
|
+
* onCleanup(
|
|
1474
|
+
* subscribeSourceToSignal(source, (snap) => {
|
|
1475
|
+
* this.isActive.set(snap);
|
|
1476
|
+
* this.updateDom();
|
|
1477
|
+
* }),
|
|
1478
|
+
* );
|
|
1479
|
+
* });
|
|
1480
|
+
* ```
|
|
1481
|
+
*/
|
|
1482
|
+
function subscribeSourceToSignal(source, onSnapshot) {
|
|
1483
|
+
onSnapshot(source.getSnapshot());
|
|
1484
|
+
const unsub = source.subscribe(() => {
|
|
1485
|
+
onSnapshot(source.getSnapshot());
|
|
1486
|
+
});
|
|
1487
|
+
return () => {
|
|
1488
|
+
unsub();
|
|
1489
|
+
source.destroy();
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const EMPTY_SNAPSHOT = {
|
|
936
1494
|
route: undefined,
|
|
937
1495
|
previousRoute: undefined,
|
|
938
|
-
}
|
|
1496
|
+
};
|
|
939
1497
|
class RouteView {
|
|
940
1498
|
nodeName = input("", { ...(ngDevMode ? { debugName: "nodeName" } : /* istanbul ignore next */ {}), alias: "routeNode" });
|
|
941
1499
|
matches = contentChildren(RouteMatch, { ...(ngDevMode ? { debugName: "matches" } : /* istanbul ignore next */ {}), descendants: true });
|
|
942
1500
|
selfs = contentChildren(RouteSelf, { ...(ngDevMode ? { debugName: "selfs" } : /* istanbul ignore next */ {}), descendants: true });
|
|
943
1501
|
notFounds = contentChildren(RouteNotFound, { ...(ngDevMode ? { debugName: "notFounds" } : /* istanbul ignore next */ {}), descendants: true });
|
|
944
|
-
activeTemplate = computed(() => {
|
|
945
|
-
|
|
946
|
-
|
|
1502
|
+
activeTemplate = computed(() => this.matchedTemplate() ?? this.fallbackTemplate(), ...(ngDevMode ? [{ debugName: "activeTemplate" }] : /* istanbul ignore next */ []));
|
|
1503
|
+
router = injectRouter();
|
|
1504
|
+
routeState = signal(EMPTY_SNAPSHOT, ...(ngDevMode ? [{ debugName: "routeState" }] : /* istanbul ignore next */ []));
|
|
1505
|
+
matchEntries = computed(() => {
|
|
1506
|
+
const nodeName = this.nodeName();
|
|
1507
|
+
return this.matches().map((match) => {
|
|
1508
|
+
const segment = match.routeMatch();
|
|
1509
|
+
return {
|
|
1510
|
+
match,
|
|
1511
|
+
fullSegmentName: nodeName ? `${nodeName}.${segment}` : segment,
|
|
1512
|
+
};
|
|
1513
|
+
});
|
|
1514
|
+
}, ...(ngDevMode ? [{ debugName: "matchEntries" }] : /* istanbul ignore next */ []));
|
|
1515
|
+
// The matched template (Match priority) is independent of the Self /
|
|
1516
|
+
// NotFound fallback chain. Splitting the two paths into separate computeds
|
|
1517
|
+
// localises re-runs: a change to `selfs()` / `notFounds()` no longer
|
|
1518
|
+
// re-evaluates the Match loop (review §8a LOW — RouteView activeTemplate
|
|
1519
|
+
// split).
|
|
1520
|
+
matchedTemplate = computed(() => {
|
|
1521
|
+
const route = this.routeState().route;
|
|
947
1522
|
if (!route) {
|
|
948
1523
|
return null;
|
|
949
1524
|
}
|
|
950
1525
|
const routeName = route.name;
|
|
951
|
-
const
|
|
952
|
-
for (const { match, fullSegmentName } of entries) {
|
|
1526
|
+
for (const { match, fullSegmentName } of this.matchEntries()) {
|
|
953
1527
|
if (startsWithSegment(routeName, fullSegmentName)) {
|
|
954
1528
|
return match.templateRef;
|
|
955
1529
|
}
|
|
956
1530
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1531
|
+
return null;
|
|
1532
|
+
}, ...(ngDevMode ? [{ debugName: "matchedTemplate" }] : /* istanbul ignore next */ []));
|
|
1533
|
+
// Fallback chain — only consulted when `matchedTemplate()` returned `null`.
|
|
1534
|
+
// Template priority: Self → NotFound. Selection rules differ on purpose:
|
|
1535
|
+
// - **Self uses first-wins** (`.at(0)`) for parity with React / Preact /
|
|
1536
|
+
// Solid / Vue, where the first matching `<Self>` token in declaration
|
|
1537
|
+
// order wins.
|
|
1538
|
+
// - **NotFound uses last-wins** (`.at(-1)`) intentionally — the fallback
|
|
1539
|
+
// should be the most-recently-declared template so that consumers can
|
|
1540
|
+
// override an inherited `<ng-template routeNotFound>` simply by
|
|
1541
|
+
// re-declaring it lower in the projected content.
|
|
1542
|
+
fallbackTemplate = computed(() => {
|
|
1543
|
+
const route = this.routeState().route;
|
|
1544
|
+
if (!route) {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
const routeName = route.name;
|
|
961
1548
|
if (routeName === this.nodeName()) {
|
|
962
1549
|
const first = this.selfs().at(0);
|
|
963
1550
|
if (first) {
|
|
@@ -971,39 +1558,25 @@ class RouteView {
|
|
|
971
1558
|
}
|
|
972
1559
|
}
|
|
973
1560
|
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();
|
|
1561
|
+
}, ...(ngDevMode ? [{ debugName: "fallbackTemplate" }] : /* istanbul ignore next */ []));
|
|
1562
|
+
constructor() {
|
|
1563
|
+
// Reactive source-creation effect (#630 fix) — see
|
|
1564
|
+
// `packages/angular/CLAUDE.md` → "Directives use constructor + effect()".
|
|
1565
|
+
effect((onCleanup) => {
|
|
1566
|
+
const source = createRouteNodeSource(this.router, this.nodeName());
|
|
1567
|
+
onCleanup(subscribeSourceToSignal(source, (snap) => {
|
|
1568
|
+
this.routeState.set(snap);
|
|
1569
|
+
}));
|
|
997
1570
|
});
|
|
998
1571
|
}
|
|
999
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1000
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1572
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1573
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", 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
1574
|
@if (activeTemplate()) {
|
|
1002
1575
|
<ng-container [ngTemplateOutlet]="activeTemplate()!" />
|
|
1003
1576
|
}
|
|
1004
1577
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1005
1578
|
}
|
|
1006
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1579
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteView, decorators: [{
|
|
1007
1580
|
type: Component,
|
|
1008
1581
|
args: [{
|
|
1009
1582
|
selector: "route-view",
|
|
@@ -1014,7 +1587,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1014
1587
|
`,
|
|
1015
1588
|
imports: [NgTemplateOutlet],
|
|
1016
1589
|
}]
|
|
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 }] }] } });
|
|
1590
|
+
}], 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
1591
|
|
|
1019
1592
|
class RouterErrorBoundary {
|
|
1020
1593
|
errorTemplate = input(...(ngDevMode ? [undefined, { debugName: "errorTemplate" }] : /* istanbul ignore next */ []));
|
|
@@ -1032,6 +1605,12 @@ class RouterErrorBoundary {
|
|
|
1032
1605
|
router = injectRouter();
|
|
1033
1606
|
snapshot = sourceToSignal(createDismissableError(this.router));
|
|
1034
1607
|
constructor() {
|
|
1608
|
+
// `effect()` registers itself with the current injection context's
|
|
1609
|
+
// `DestroyRef` and tears down automatically when the component is
|
|
1610
|
+
// destroyed. The earlier manual `effectRef.destroy()` wired through
|
|
1611
|
+
// `inject(DestroyRef).onDestroy(...)` duplicated that built-in cleanup
|
|
1612
|
+
// (audit §8.1 LOW — confirmed: no behavior change without the manual
|
|
1613
|
+
// path).
|
|
1035
1614
|
effect(() => {
|
|
1036
1615
|
const snap = this.snapshot();
|
|
1037
1616
|
if (snap.error) {
|
|
@@ -1043,8 +1622,8 @@ class RouterErrorBoundary {
|
|
|
1043
1622
|
}
|
|
1044
1623
|
});
|
|
1045
1624
|
}
|
|
1046
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1047
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1625
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouterErrorBoundary, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1626
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", 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
1627
|
<ng-content />
|
|
1049
1628
|
@if (errorContext() && errorTemplate()) {
|
|
1050
1629
|
<ng-container
|
|
@@ -1054,7 +1633,7 @@ class RouterErrorBoundary {
|
|
|
1054
1633
|
}
|
|
1055
1634
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1056
1635
|
}
|
|
1057
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1636
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouterErrorBoundary, decorators: [{
|
|
1058
1637
|
type: Component,
|
|
1059
1638
|
args: [{
|
|
1060
1639
|
selector: "router-error-boundary",
|
|
@@ -1078,10 +1657,10 @@ class NavigationAnnouncer {
|
|
|
1078
1657
|
this.announcer.destroy();
|
|
1079
1658
|
});
|
|
1080
1659
|
}
|
|
1081
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1082
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.
|
|
1660
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NavigationAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1661
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.13", type: NavigationAnnouncer, isStandalone: true, selector: "navigation-announcer", ngImport: i0, template: "", isInline: true });
|
|
1083
1662
|
}
|
|
1084
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1663
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NavigationAnnouncer, decorators: [{
|
|
1085
1664
|
type: Component,
|
|
1086
1665
|
args: [{
|
|
1087
1666
|
selector: "navigation-announcer",
|
|
@@ -1089,6 +1668,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1089
1668
|
}]
|
|
1090
1669
|
}], ctorParameters: () => [] });
|
|
1091
1670
|
|
|
1671
|
+
const NOOP_CATCH = () => { };
|
|
1092
1672
|
class RealLink {
|
|
1093
1673
|
routeName = input("", ...(ngDevMode ? [{ debugName: "routeName" }] : /* istanbul ignore next */ []));
|
|
1094
1674
|
routeParams = input({}, ...(ngDevMode ? [{ debugName: "routeParams" }] : /* istanbul ignore next */ []));
|
|
@@ -1104,38 +1684,45 @@ class RealLink {
|
|
|
1104
1684
|
*/
|
|
1105
1685
|
hash = input(undefined, ...(ngDevMode ? [{ debugName: "hash" }] : /* istanbul ignore next */ []));
|
|
1106
1686
|
router = injectRouter();
|
|
1107
|
-
destroyRef = inject(DestroyRef);
|
|
1108
1687
|
anchor = inject(ElementRef)
|
|
1109
1688
|
.nativeElement;
|
|
1110
1689
|
isActive = signal(false, ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
|
|
1690
|
+
// `href` is computed from signal inputs only — Angular's default Object.is
|
|
1691
|
+
// equality already collapses repeated `string` results, so no custom
|
|
1692
|
+
// comparator is required (review §8b note 3 — applies after verifying that
|
|
1693
|
+
// `buildHref` returns a primitive).
|
|
1111
1694
|
href = computed(() => {
|
|
1112
1695
|
const hashValue = this.hash();
|
|
1113
1696
|
return buildHref(this.router, this.routeName(), this.routeParams(), hashValue === undefined ? undefined : { hash: hashValue });
|
|
1114
1697
|
}, ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
1115
1698
|
prevActiveClass = "";
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1699
|
+
prevHref = undefined;
|
|
1700
|
+
// Skip-same-value: only re-touch the DOM `class` list when the active state
|
|
1701
|
+
// actually flipped. Without this, every navigation that re-fires the active
|
|
1702
|
+
// source still issues a `classList.toggle` no-op (review §8b MEDIUM).
|
|
1703
|
+
prevActive = undefined;
|
|
1704
|
+
constructor() {
|
|
1705
|
+
// Reactive source-creation effect (#630 fix) — see
|
|
1706
|
+
// `packages/angular/CLAUDE.md` → "Directives use constructor + effect()".
|
|
1707
|
+
// Reading signal inputs inside `effect()` re-creates the active-route
|
|
1708
|
+
// source whenever any input changes; `onCleanup` tears the previous
|
|
1709
|
+
// subscription down.
|
|
1710
|
+
effect((onCleanup) => {
|
|
1711
|
+
const source = createActiveRouteSource(this.router, this.routeName(), this.routeParams(), buildActiveRouteOptions(this.activeStrict(), this.ignoreQueryParams(), this.hash()));
|
|
1712
|
+
onCleanup(subscribeSourceToSignal(source, (snap) => {
|
|
1713
|
+
// Pure-href refresh: when the active flag did not change, only the
|
|
1714
|
+
// href may have moved (e.g. param-only update on a parent route).
|
|
1715
|
+
// Skip the classList work in that branch (review §8b MEDIUM).
|
|
1716
|
+
if (snap === this.prevActive) {
|
|
1717
|
+
this.isActive.set(snap);
|
|
1718
|
+
this.updateHref();
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
this.prevActive = snap;
|
|
1722
|
+
this.isActive.set(snap);
|
|
1723
|
+
this.updateHref();
|
|
1724
|
+
this.updateActiveClass();
|
|
1725
|
+
}));
|
|
1139
1726
|
});
|
|
1140
1727
|
}
|
|
1141
1728
|
onClick(event) {
|
|
@@ -1143,13 +1730,16 @@ class RealLink {
|
|
|
1143
1730
|
return;
|
|
1144
1731
|
}
|
|
1145
1732
|
event.preventDefault();
|
|
1146
|
-
navigateWithHash(this.router, this.routeName(), this.routeParams(), this.hash(), this.routeOptions()).catch(
|
|
1733
|
+
navigateWithHash(this.router, this.routeName(), this.routeParams(), this.hash(), this.routeOptions()).catch(NOOP_CATCH);
|
|
1147
1734
|
}
|
|
1148
|
-
|
|
1735
|
+
updateHref() {
|
|
1149
1736
|
const href = this.href();
|
|
1150
|
-
if (href !== undefined) {
|
|
1737
|
+
if (href !== undefined && href !== this.prevHref) {
|
|
1151
1738
|
this.anchor.setAttribute("href", href);
|
|
1152
1739
|
}
|
|
1740
|
+
this.prevHref = href;
|
|
1741
|
+
}
|
|
1742
|
+
updateActiveClass() {
|
|
1153
1743
|
const activeClass = this.activeClassName();
|
|
1154
1744
|
if (this.prevActiveClass && this.prevActiveClass !== activeClass) {
|
|
1155
1745
|
this.anchor.classList.remove(this.prevActiveClass);
|
|
@@ -1159,10 +1749,10 @@ class RealLink {
|
|
|
1159
1749
|
}
|
|
1160
1750
|
this.prevActiveClass = activeClass;
|
|
1161
1751
|
}
|
|
1162
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1163
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
1752
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RealLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1753
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", 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
1754
|
}
|
|
1165
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1755
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RealLink, decorators: [{
|
|
1166
1756
|
type: Directive,
|
|
1167
1757
|
args: [{
|
|
1168
1758
|
selector: "a[realLink]",
|
|
@@ -1170,7 +1760,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1170
1760
|
"(click)": "onClick($event)",
|
|
1171
1761
|
},
|
|
1172
1762
|
}]
|
|
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 }] }] } });
|
|
1763
|
+
}], 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
1764
|
|
|
1175
1765
|
class RealLinkActive {
|
|
1176
1766
|
realLinkActive = input("", ...(ngDevMode ? [{ debugName: "realLinkActive" }] : /* istanbul ignore next */ []));
|
|
@@ -1179,26 +1769,29 @@ class RealLinkActive {
|
|
|
1179
1769
|
activeStrict = input(false, ...(ngDevMode ? [{ debugName: "activeStrict" }] : /* istanbul ignore next */ []));
|
|
1180
1770
|
ignoreQueryParams = input(true, ...(ngDevMode ? [{ debugName: "ignoreQueryParams" }] : /* istanbul ignore next */ []));
|
|
1181
1771
|
router = injectRouter();
|
|
1182
|
-
destroyRef = inject(DestroyRef);
|
|
1183
1772
|
element = inject(ElementRef).nativeElement;
|
|
1184
1773
|
isActive = signal(false, ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
|
|
1774
|
+
// Skip-same-value: only touch `classList.toggle` when the active flag
|
|
1775
|
+
// actually flipped. Saves one DOM write per RealLinkActive per unrelated
|
|
1776
|
+
// navigation (review §8b MEDIUM).
|
|
1777
|
+
prevActive = undefined;
|
|
1185
1778
|
constructor() {
|
|
1779
|
+
// One-time a11y setup — doesn't depend on signal inputs, stays in
|
|
1780
|
+
// constructor body. `applyLinkA11y` is idempotent so re-running would
|
|
1781
|
+
// be safe, but we only need it once per element.
|
|
1186
1782
|
applyLinkA11y(this.element);
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
this.destroyRef.onDestroy(() => {
|
|
1200
|
-
unsub();
|
|
1201
|
-
source.destroy();
|
|
1783
|
+
// Reactive source-creation effect (#630 fix) — see
|
|
1784
|
+
// `packages/angular/CLAUDE.md` → "Directives use constructor + effect()".
|
|
1785
|
+
effect((onCleanup) => {
|
|
1786
|
+
const source = createActiveRouteSource(this.router, this.routeName(), this.routeParams(), buildActiveRouteOptions(this.activeStrict(), this.ignoreQueryParams(), undefined));
|
|
1787
|
+
onCleanup(subscribeSourceToSignal(source, (snap) => {
|
|
1788
|
+
if (snap === this.prevActive) {
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
this.prevActive = snap;
|
|
1792
|
+
this.isActive.set(snap);
|
|
1793
|
+
this.updateClass();
|
|
1794
|
+
}));
|
|
1202
1795
|
});
|
|
1203
1796
|
}
|
|
1204
1797
|
updateClass() {
|
|
@@ -1208,10 +1801,10 @@ class RealLinkActive {
|
|
|
1208
1801
|
}
|
|
1209
1802
|
this.element.classList.toggle(className, this.isActive());
|
|
1210
1803
|
}
|
|
1211
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1212
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
1804
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RealLinkActive, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1805
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", 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
1806
|
}
|
|
1214
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1807
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RealLinkActive, decorators: [{
|
|
1215
1808
|
type: Directive,
|
|
1216
1809
|
args: [{ selector: "[realLinkActive]" }]
|
|
1217
1810
|
}], 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 +1813,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1220
1813
|
* Generated bundle index. Do not edit.
|
|
1221
1814
|
*/
|
|
1222
1815
|
|
|
1223
|
-
export { NAVIGATOR, NavigationAnnouncer, ROUTE, ROUTER, RealLink, RealLinkActive, RouteMatch, RouteNotFound, RouteSelf, RouteView, RouterErrorBoundary, injectIsActiveRoute, injectNavigator, injectRoute, injectRouteEnter, injectRouteExit, injectRouteNode, injectRouteUtils, injectRouter, injectRouterTransition, provideRealRouter, sourceToSignal };
|
|
1816
|
+
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
1817
|
//# sourceMappingURL=real-router-angular.mjs.map
|