@mmstack/router-core 21.0.3 → 21.0.4
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 +352 -201
- package/fesm2022/mmstack-router-core.mjs +331 -60
- package/fesm2022/mmstack-router-core.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-router-core.d.ts +217 -32
|
@@ -1,42 +1,13 @@
|
|
|
1
|
-
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, computed, Injectable, InjectionToken, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, linkedSignal } from '@angular/core';
|
|
3
1
|
import * as i1 from '@angular/router';
|
|
4
2
|
import { PRIMARY_OUTLET, EventType, Router, createUrlTreeFromSnapshot, UrlTree, RouterLink, RouterLinkWithHref, ActivatedRoute } from '@angular/router';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { inject, computed, Injectable, InjectionToken, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, linkedSignal } from '@angular/core';
|
|
5
5
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
6
6
|
import { filter, map } from 'rxjs/operators';
|
|
7
7
|
import { mutable, mapArray, until, elementVisibility, toWritable } from '@mmstack/primitives';
|
|
8
8
|
import { Subject, EMPTY, filter as filter$1, take, switchMap, finalize } from 'rxjs';
|
|
9
9
|
import { Title } from '@angular/platform-browser';
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* @internal
|
|
13
|
-
*/
|
|
14
|
-
const INTERNAL_BREADCRUMB_SYMBOL = Symbol.for('MMSTACK_INTERNAL_BREADCRUMB');
|
|
15
|
-
/**
|
|
16
|
-
* @internal
|
|
17
|
-
*/
|
|
18
|
-
function getBreadcrumbInternals(breadcrumb) {
|
|
19
|
-
return breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* @internal
|
|
23
|
-
*/
|
|
24
|
-
function createInternalBreadcrumb(bc, active, registered = true) {
|
|
25
|
-
return {
|
|
26
|
-
...bc,
|
|
27
|
-
[INTERNAL_BREADCRUMB_SYMBOL]: {
|
|
28
|
-
active,
|
|
29
|
-
registered,
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* @internal
|
|
35
|
-
*/
|
|
36
|
-
function isInternalBreadcrumb(breadcrumb) {
|
|
37
|
-
return !!breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
11
|
function parsePathSegment$1(segmentString) {
|
|
41
12
|
const parts = segmentString.split(';');
|
|
42
13
|
const pathPart = parts[0];
|
|
@@ -252,8 +223,9 @@ function isNavigationEnd(e) {
|
|
|
252
223
|
* }
|
|
253
224
|
* ```
|
|
254
225
|
*/
|
|
255
|
-
function url() {
|
|
256
|
-
|
|
226
|
+
function url(router) {
|
|
227
|
+
if (!router)
|
|
228
|
+
router = inject(Router);
|
|
257
229
|
return toSignal(router.events.pipe(filter(isNavigationEnd), map((e) => e.urlAfterRedirects)), {
|
|
258
230
|
initialValue: router.url,
|
|
259
231
|
});
|
|
@@ -327,7 +299,36 @@ function injectSnapshotPathResolver() {
|
|
|
327
299
|
/**
|
|
328
300
|
* @internal
|
|
329
301
|
*/
|
|
330
|
-
const
|
|
302
|
+
const INTERNAL_BREADCRUMB_SYMBOL = Symbol.for('MMSTACK_INTERNAL_BREADCRUMB');
|
|
303
|
+
/**
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
function getBreadcrumbInternals(breadcrumb) {
|
|
307
|
+
return breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* @internal
|
|
311
|
+
*/
|
|
312
|
+
function createInternalBreadcrumb(bc, active, registered = true) {
|
|
313
|
+
return {
|
|
314
|
+
...bc,
|
|
315
|
+
[INTERNAL_BREADCRUMB_SYMBOL]: {
|
|
316
|
+
active,
|
|
317
|
+
registered,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* @internal
|
|
323
|
+
*/
|
|
324
|
+
function isInternalBreadcrumb(breadcrumb) {
|
|
325
|
+
return !!breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* @internal
|
|
330
|
+
*/
|
|
331
|
+
const token$2 = new InjectionToken('@mmstack/router-core:breadcrumb-config');
|
|
331
332
|
/**
|
|
332
333
|
* Provides configuration for the breadcrumb system.
|
|
333
334
|
* @param config - A partial `BreadcrumbConfig` object with the desired settings. *
|
|
@@ -359,7 +360,7 @@ const token$1 = new InjectionToken('MMSTACK_BREADCRUMB_CONFIG');
|
|
|
359
360
|
*/
|
|
360
361
|
function provideBreadcrumbConfig(config) {
|
|
361
362
|
return {
|
|
362
|
-
provide: token$
|
|
363
|
+
provide: token$2,
|
|
363
364
|
useValue: {
|
|
364
365
|
...config,
|
|
365
366
|
},
|
|
@@ -369,7 +370,7 @@ function provideBreadcrumbConfig(config) {
|
|
|
369
370
|
* @internal
|
|
370
371
|
*/
|
|
371
372
|
function injectBreadcrumbConfig() {
|
|
372
|
-
return (inject(token$
|
|
373
|
+
return (inject(token$2, {
|
|
373
374
|
optional: true,
|
|
374
375
|
}) ?? {});
|
|
375
376
|
}
|
|
@@ -515,10 +516,11 @@ function injectBreadcrumbs() {
|
|
|
515
516
|
/**
|
|
516
517
|
* Creates and registers a breadcrumb for a specific route.
|
|
517
518
|
* This function is designed to be used as an Angular Route `ResolveFn`.
|
|
518
|
-
* It handles the registration of the breadcrumb with the `BreadcrumbStore`
|
|
519
|
-
* and ensures automatic deregistration when the route is destroyed.
|
|
520
519
|
*
|
|
521
|
-
*
|
|
520
|
+
* Accepts a static label (`string`), a static options object, or a factory returning either —
|
|
521
|
+
* use a factory when you need `inject()` for dynamic data.
|
|
522
|
+
*
|
|
523
|
+
* @param factoryOrValue A static label, a static `CreateBreadcrumbOptions`, or a factory returning either.
|
|
522
524
|
* @see CreateBreadcrumbOptions
|
|
523
525
|
*
|
|
524
526
|
* @example
|
|
@@ -528,25 +530,34 @@ function injectBreadcrumbs() {
|
|
|
528
530
|
* path: 'home',
|
|
529
531
|
* component: HomeComponent,
|
|
530
532
|
* resolve: {
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
* });
|
|
533
|
+
* // shorthand for { label: 'Home' }
|
|
534
|
+
* breadcrumb: createBreadcrumb('Home'),
|
|
534
535
|
* },
|
|
536
|
+
* },
|
|
537
|
+
* {
|
|
535
538
|
* path: 'users/:userId',
|
|
536
539
|
* component: UserProfileComponent,
|
|
537
540
|
* resolve: {
|
|
538
541
|
* breadcrumb: createBreadcrumb(() => {
|
|
539
542
|
* const userStore = inject(UserStore);
|
|
540
543
|
* return {
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
+
* label: () => userStore.user().name ?? 'Loading...',
|
|
545
|
+
* };
|
|
546
|
+
* }),
|
|
544
547
|
* },
|
|
545
|
-
* }
|
|
548
|
+
* },
|
|
546
549
|
* ];
|
|
547
550
|
* ```
|
|
548
551
|
*/
|
|
549
|
-
function createBreadcrumb(
|
|
552
|
+
function createBreadcrumb(factoryOrValue) {
|
|
553
|
+
const factory = typeof factoryOrValue === 'string'
|
|
554
|
+
? () => ({ label: factoryOrValue })
|
|
555
|
+
: typeof factoryOrValue === 'function'
|
|
556
|
+
? () => {
|
|
557
|
+
const result = factoryOrValue();
|
|
558
|
+
return typeof result === 'string' ? { label: result } : result;
|
|
559
|
+
}
|
|
560
|
+
: () => factoryOrValue;
|
|
550
561
|
return async (route) => {
|
|
551
562
|
const router = inject(Router);
|
|
552
563
|
const store = inject(BreadcrumbStore);
|
|
@@ -650,6 +661,38 @@ function treeToSerializedUrl(router, urlTree) {
|
|
|
650
661
|
return null;
|
|
651
662
|
return router.serializeUrl(urlTree);
|
|
652
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Returns an imperative function that triggers preloading for an arbitrary link, using
|
|
666
|
+
* the same path resolution and {@link PreloadStrategy} pipeline as the {@link Link}
|
|
667
|
+
* (`mmLink`) directive.
|
|
668
|
+
*
|
|
669
|
+
* Use this when the `Link` directive isn't a fit — for example, preloading a route from
|
|
670
|
+
* an effect when a user opens a menu, hovers a non-link element, or reacts to a signal
|
|
671
|
+
* change — and you don't want to render an `<a [mmLink]>` just to request the preload.
|
|
672
|
+
*
|
|
673
|
+
* Requires {@link PreloadStrategy} to be wired up via `provideRouter(routes, withPreloading(PreloadStrategy))`,
|
|
674
|
+
* just like the directive.
|
|
675
|
+
*
|
|
676
|
+
* @returns A function accepting the same link descriptor shape as `mmLink` (`string`,
|
|
677
|
+
* commands array, `UrlTree`, or `null`). Passing `null` or an unresolvable link is a no-op.
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* ```typescript
|
|
681
|
+
* @Component({ ... })
|
|
682
|
+
* export class CommandPaletteComponent {
|
|
683
|
+
* private readonly triggerPreload = injectTriggerPreload();
|
|
684
|
+
*
|
|
685
|
+
* protected readonly highlighted = signal<string | null>(null);
|
|
686
|
+
*
|
|
687
|
+
* constructor() {
|
|
688
|
+
* effect(() => {
|
|
689
|
+
* const target = this.highlighted();
|
|
690
|
+
* if (target) this.triggerPreload(target);
|
|
691
|
+
* });
|
|
692
|
+
* }
|
|
693
|
+
* }
|
|
694
|
+
* ```
|
|
695
|
+
*/
|
|
653
696
|
function injectTriggerPreload() {
|
|
654
697
|
const req = inject(PreloadRequester);
|
|
655
698
|
const router = inject(Router);
|
|
@@ -791,6 +834,229 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
791
834
|
]]
|
|
792
835
|
}] } });
|
|
793
836
|
|
|
837
|
+
/** @internal */
|
|
838
|
+
const NEVER_TRUE = computed(() => false, ...(ngDevMode ? [{ debugName: "NEVER_TRUE" }] : /* istanbul ignore next */ []));
|
|
839
|
+
function wrap(value, fallback) {
|
|
840
|
+
if (value === undefined)
|
|
841
|
+
return fallback ? fallback : computed(() => undefined);
|
|
842
|
+
if (typeof value === 'function')
|
|
843
|
+
return isSignal(value) ? value : computed(value);
|
|
844
|
+
return computed(() => value);
|
|
845
|
+
}
|
|
846
|
+
function isAbsoluteCommandArray(commands) {
|
|
847
|
+
return (commands.length > 0 &&
|
|
848
|
+
typeof commands[0] === 'string' &&
|
|
849
|
+
commands[0].startsWith('/'));
|
|
850
|
+
}
|
|
851
|
+
function resolveLinkTree(input, router, relativeTo) {
|
|
852
|
+
const raw = typeof input === 'function' ? input() : input;
|
|
853
|
+
if (raw === undefined || raw === null)
|
|
854
|
+
return null;
|
|
855
|
+
if (raw instanceof UrlTree)
|
|
856
|
+
return raw;
|
|
857
|
+
if (typeof raw === 'string') {
|
|
858
|
+
if (raw.startsWith('/'))
|
|
859
|
+
return router.parseUrl(raw);
|
|
860
|
+
const parsed = router.parseUrl('/' + raw);
|
|
861
|
+
const primary = parsed.root.children['primary'];
|
|
862
|
+
const segments = primary ? primary.segments.map((s) => s.path) : [];
|
|
863
|
+
return createUrlTreeFromSnapshot(relativeTo, segments, parsed.queryParams, parsed.fragment);
|
|
864
|
+
}
|
|
865
|
+
if (isAbsoluteCommandArray(raw))
|
|
866
|
+
return router.createUrlTree(raw);
|
|
867
|
+
return createUrlTreeFromSnapshot(relativeTo, raw);
|
|
868
|
+
}
|
|
869
|
+
function resolveMeta(input) {
|
|
870
|
+
if (input === undefined)
|
|
871
|
+
return {};
|
|
872
|
+
return typeof input === 'function' ? input() : input;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* @internal
|
|
876
|
+
* Recursively builds an {@link InternalNavItem} tree from {@link CreateNavItem} input.
|
|
877
|
+
* Cascades parent `disabled`/`hidden` to descendants and computes `active` against the
|
|
878
|
+
* current router URL using `Router.isActive`.
|
|
879
|
+
*/
|
|
880
|
+
function createInternalNavItem(input, router, relativeTo, configActiveMatch, parentDisabled, parentHidden, fallbackId) {
|
|
881
|
+
const label = wrap(input.label);
|
|
882
|
+
const ariaLabel = input.ariaLabel ? wrap(input.ariaLabel) : label;
|
|
883
|
+
const linkTree = computed(() => resolveLinkTree(input.link, router, relativeTo), ...(ngDevMode ? [{ debugName: "linkTree" }] : /* istanbul ignore next */ []));
|
|
884
|
+
const link = computed(() => {
|
|
885
|
+
const tree = linkTree();
|
|
886
|
+
return tree ? router.serializeUrl(tree) : null;
|
|
887
|
+
}, ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
|
|
888
|
+
const ownDisabled = wrap(input.disabled, NEVER_TRUE);
|
|
889
|
+
const ownHidden = wrap(input.hidden, NEVER_TRUE);
|
|
890
|
+
const disabled = computed(() => parentDisabled() || ownDisabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
891
|
+
const hidden = computed(() => parentHidden() || ownHidden(), ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
892
|
+
const metaInput = input.meta;
|
|
893
|
+
const meta = computed(() => resolveMeta(metaInput), ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
|
|
894
|
+
const id = input.id !== undefined
|
|
895
|
+
? wrap(input.id)
|
|
896
|
+
: computed(() => link() ?? fallbackId);
|
|
897
|
+
const childItems = (input.children ?? []).map((childInput, i) => createInternalNavItem(childInput, router, relativeTo, configActiveMatch, disabled, hidden, `${fallbackId}.${i}`));
|
|
898
|
+
const children = computed(() => childItems.filter((c) => !c.hidden()), ...(ngDevMode ? [{ debugName: "children" }] : /* istanbul ignore next */ []));
|
|
899
|
+
const mergedActiveMatch = {
|
|
900
|
+
...configActiveMatch,
|
|
901
|
+
...input.activeMatch,
|
|
902
|
+
};
|
|
903
|
+
const trackNavigation = url(router);
|
|
904
|
+
const finalOptions = {
|
|
905
|
+
paths: 'subset',
|
|
906
|
+
fragment: 'ignored',
|
|
907
|
+
matrixParams: 'ignored',
|
|
908
|
+
queryParams: 'subset',
|
|
909
|
+
...mergedActiveMatch,
|
|
910
|
+
};
|
|
911
|
+
const ownActive = computed(() => {
|
|
912
|
+
trackNavigation();
|
|
913
|
+
const tree = linkTree();
|
|
914
|
+
return tree ? router.isActive(tree, finalOptions) : false;
|
|
915
|
+
}, ...(ngDevMode ? [{ debugName: "ownActive" }] : /* istanbul ignore next */ []));
|
|
916
|
+
const orWithChildren = input.matchesWhenChildActive ?? input.activeMatch === undefined;
|
|
917
|
+
const active = computed(() => ownActive() ||
|
|
918
|
+
(orWithChildren &&
|
|
919
|
+
!!childItems.length &&
|
|
920
|
+
childItems.some((c) => c.active())), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
921
|
+
return {
|
|
922
|
+
id,
|
|
923
|
+
label,
|
|
924
|
+
ariaLabel,
|
|
925
|
+
link,
|
|
926
|
+
active,
|
|
927
|
+
disabled,
|
|
928
|
+
hidden,
|
|
929
|
+
meta,
|
|
930
|
+
children,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** @internal */
|
|
935
|
+
const token$1 = new InjectionToken('@mmstack/router-core:nav-config');
|
|
936
|
+
/**
|
|
937
|
+
* Provides global configuration for the nav system.
|
|
938
|
+
*
|
|
939
|
+
* @example
|
|
940
|
+
* ```typescript
|
|
941
|
+
* provideNavConfig({
|
|
942
|
+
* activeMatch: { queryParams: 'ignored' },
|
|
943
|
+
* }),
|
|
944
|
+
* ```
|
|
945
|
+
*/
|
|
946
|
+
function provideNavConfig(config) {
|
|
947
|
+
return {
|
|
948
|
+
provide: token$1,
|
|
949
|
+
useValue: { ...config },
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/** @internal */
|
|
953
|
+
function injectNavConfig() {
|
|
954
|
+
return inject(token$1, { optional: true }) ?? {};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** @internal */
|
|
958
|
+
const DEFAULT_NAV_SCOPE = Symbol('mmstack.nav.default');
|
|
959
|
+
class NavStore {
|
|
960
|
+
map = mutable(new Map());
|
|
961
|
+
leafRoutes = injectLeafRoutes();
|
|
962
|
+
/** @internal */
|
|
963
|
+
register(scope, routePath, items) {
|
|
964
|
+
this.map.inline((m) => {
|
|
965
|
+
let scopeMap = m.get(scope);
|
|
966
|
+
if (!scopeMap) {
|
|
967
|
+
scopeMap = new Map();
|
|
968
|
+
m.set(scope, scopeMap);
|
|
969
|
+
}
|
|
970
|
+
scopeMap.set(routePath, items);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
/** @internal */
|
|
974
|
+
scope(name) {
|
|
975
|
+
return computed(() => {
|
|
976
|
+
const scopeMap = this.map().get(name);
|
|
977
|
+
if (!scopeMap)
|
|
978
|
+
return [];
|
|
979
|
+
const leaves = this.leafRoutes();
|
|
980
|
+
for (let i = leaves.length - 1; i >= 0; i--) {
|
|
981
|
+
const items = scopeMap.get(leaves[i].path);
|
|
982
|
+
if (items) {
|
|
983
|
+
return items.filter((it) => !it.hidden());
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return [];
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
990
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, providedIn: 'root' });
|
|
991
|
+
}
|
|
992
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, decorators: [{
|
|
993
|
+
type: Injectable,
|
|
994
|
+
args: [{ providedIn: 'root' }]
|
|
995
|
+
}] });
|
|
996
|
+
/**
|
|
997
|
+
* Returns a reactive list of nav items for the requested scope.
|
|
998
|
+
*
|
|
999
|
+
* The returned signal reflects the nearest active ancestor route that registered items
|
|
1000
|
+
* for `name` via `createNavItems`. Hidden items are filtered out.
|
|
1001
|
+
*
|
|
1002
|
+
* @typeParam TMeta The shape of `NavItem.meta` for the consuming code. Untyped at the
|
|
1003
|
+
* registration site — this is a consumer-side assertion.
|
|
1004
|
+
*
|
|
1005
|
+
* @example
|
|
1006
|
+
* ```typescript
|
|
1007
|
+
* @Component({
|
|
1008
|
+
* template: `
|
|
1009
|
+
* @for (item of items(); track item) {
|
|
1010
|
+
* <a [href]="item.link()" [class.active]="item.active()">{{ item.label() }}</a>
|
|
1011
|
+
* }
|
|
1012
|
+
* `,
|
|
1013
|
+
* })
|
|
1014
|
+
* export class TopBar {
|
|
1015
|
+
* protected readonly items = injectNavItems();
|
|
1016
|
+
* }
|
|
1017
|
+
* ```
|
|
1018
|
+
*/
|
|
1019
|
+
function injectNavItems(name) {
|
|
1020
|
+
const store = inject(NavStore);
|
|
1021
|
+
return store.scope(name ?? DEFAULT_NAV_SCOPE);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Registers a set of nav items for the activating route under the given scope.
|
|
1026
|
+
* Mirrors `createBreadcrumb` / `createTitle` — designed to be used in a route's
|
|
1027
|
+
* `resolve` map.
|
|
1028
|
+
*
|
|
1029
|
+
* Multiple scopes can be registered on a single route by giving each its own `name`
|
|
1030
|
+
* (and a unique key in the `resolve` map):
|
|
1031
|
+
*
|
|
1032
|
+
* ```typescript
|
|
1033
|
+
* resolve: {
|
|
1034
|
+
* mainNav: createNavItems([...], { name: 'main' }),
|
|
1035
|
+
* sideNav: createNavItems([...], { name: 'side' }),
|
|
1036
|
+
* }
|
|
1037
|
+
* ```
|
|
1038
|
+
*
|
|
1039
|
+
* Scope override semantics: when multiple routes in the active chain register items
|
|
1040
|
+
* under the same scope, the deepest active registration wins. Navigating away restores
|
|
1041
|
+
* the shallower registration.
|
|
1042
|
+
*/
|
|
1043
|
+
function createNavItems(itemsOrFactory, options) {
|
|
1044
|
+
const factory = typeof itemsOrFactory === 'function'
|
|
1045
|
+
? itemsOrFactory
|
|
1046
|
+
: () => itemsOrFactory;
|
|
1047
|
+
return async (route) => {
|
|
1048
|
+
const router = inject(Router);
|
|
1049
|
+
const store = inject(NavStore);
|
|
1050
|
+
const resolveRoutePath = injectSnapshotPathResolver();
|
|
1051
|
+
const config = injectNavConfig();
|
|
1052
|
+
const routePath = resolveRoutePath(route);
|
|
1053
|
+
const scope = options?.name ?? DEFAULT_NAV_SCOPE;
|
|
1054
|
+
const items = factory().map((input, i) => createInternalNavItem(input, router, route, config.activeMatch, NEVER_TRUE, NEVER_TRUE, `${routePath}#${i}`));
|
|
1055
|
+
store.register(scope, routePath, items);
|
|
1056
|
+
return Promise.resolve();
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
794
1060
|
/**
|
|
795
1061
|
* Creates a WritableSignal that synchronizes with a specific URL query parameter,
|
|
796
1062
|
* enabling two-way binding between the signal's state and the URL.
|
|
@@ -900,7 +1166,7 @@ function queryParam(key, route = inject(ActivatedRoute)) {
|
|
|
900
1166
|
return toWritable(queryParam, set);
|
|
901
1167
|
}
|
|
902
1168
|
|
|
903
|
-
const token = new InjectionToken('
|
|
1169
|
+
const token = new InjectionToken('@mmstack/router-core:title-config');
|
|
904
1170
|
/**
|
|
905
1171
|
* used to provide the title configuration, will not be applied unless a `createTitle` resolver is used
|
|
906
1172
|
*/
|
|
@@ -912,6 +1178,7 @@ function provideTitleConfig(config) {
|
|
|
912
1178
|
return {
|
|
913
1179
|
provide: token,
|
|
914
1180
|
useValue: {
|
|
1181
|
+
initialTitle: config?.initialTitle ?? '',
|
|
915
1182
|
parser: prefixFn,
|
|
916
1183
|
keepLastKnown: config?.keepLastKnownTitle ?? true,
|
|
917
1184
|
},
|
|
@@ -924,25 +1191,28 @@ function injectTitleConfig() {
|
|
|
924
1191
|
return (inject(token, {
|
|
925
1192
|
optional: true,
|
|
926
1193
|
}) ?? {
|
|
1194
|
+
initialTitle: '',
|
|
927
1195
|
parser: identity,
|
|
928
1196
|
keepLastKnown: true,
|
|
929
1197
|
});
|
|
930
1198
|
}
|
|
931
1199
|
|
|
932
1200
|
class TitleStore {
|
|
933
|
-
title = inject(Title);
|
|
934
1201
|
map = mutable(new Map());
|
|
935
|
-
leafRoutes = injectLeafRoutes();
|
|
936
1202
|
constructor() {
|
|
937
|
-
const
|
|
1203
|
+
const { keepLastKnown, initialTitle } = injectTitleConfig();
|
|
1204
|
+
const leafRoutes = injectLeafRoutes();
|
|
1205
|
+
const title = inject(Title);
|
|
1206
|
+
const fallbackTitle = initialTitle || untracked(() => title.getTitle());
|
|
1207
|
+
const reverseLeaves = computed(() => leafRoutes().toReversed(), ...(ngDevMode ? [{ debugName: "reverseLeaves" }] : /* istanbul ignore next */ []));
|
|
938
1208
|
const currentResolvedTitles = computed(() => {
|
|
939
1209
|
const map = this.map();
|
|
940
1210
|
return reverseLeaves()
|
|
941
|
-
.map((leaf) => map.get(leaf.path)?.() ?? leaf.route.title)
|
|
942
|
-
.filter((v) =>
|
|
1211
|
+
.map((leaf) => map.get(leaf.path)?.() ?? leaf.route.title ?? null)
|
|
1212
|
+
.filter((v) => v !== null);
|
|
943
1213
|
}, ...(ngDevMode ? [{ debugName: "currentResolvedTitles" }] : /* istanbul ignore next */ []));
|
|
944
1214
|
const currentTitle = computed(() => currentResolvedTitles().at(0) ?? '', ...(ngDevMode ? [{ debugName: "currentTitle" }] : /* istanbul ignore next */ []));
|
|
945
|
-
const heldTitle =
|
|
1215
|
+
const heldTitle = keepLastKnown
|
|
946
1216
|
? linkedSignal({
|
|
947
1217
|
source: () => currentTitle(),
|
|
948
1218
|
computation: (value, prev) => {
|
|
@@ -953,7 +1223,7 @@ class TitleStore {
|
|
|
953
1223
|
})
|
|
954
1224
|
: currentTitle;
|
|
955
1225
|
effect(() => {
|
|
956
|
-
|
|
1226
|
+
title.setTitle(heldTitle() || fallbackTitle);
|
|
957
1227
|
});
|
|
958
1228
|
}
|
|
959
1229
|
register(id, titleFn) {
|
|
@@ -972,19 +1242,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
972
1242
|
*
|
|
973
1243
|
* Creates a title resolver function that can be used in Angular's router.
|
|
974
1244
|
*
|
|
975
|
-
* @param
|
|
976
|
-
* A function that returns a string or a Signal<string> representing the title.
|
|
1245
|
+
* @param factoryOrValue
|
|
1246
|
+
* A function that returns a string or a Signal<string> representing the title or just the string directly.
|
|
977
1247
|
* @param awaitValue
|
|
978
1248
|
* If `true`, the resolver will wait until the title signal has a value before resolving.
|
|
979
1249
|
* Defaults to `false`.
|
|
980
1250
|
*/
|
|
981
|
-
function createTitle(
|
|
1251
|
+
function createTitle(factoryOrValue, awaitValue = false) {
|
|
1252
|
+
const factory = typeof factoryOrValue === 'string' ? () => factoryOrValue : factoryOrValue;
|
|
982
1253
|
return async (route) => {
|
|
983
1254
|
const store = inject(TitleStore);
|
|
984
1255
|
const resolver = injectSnapshotPathResolver();
|
|
985
1256
|
const fp = resolver(route);
|
|
986
1257
|
const { parser } = injectTitleConfig();
|
|
987
|
-
const resolved =
|
|
1258
|
+
const resolved = factory();
|
|
988
1259
|
const titleSignal = typeof resolved === 'string'
|
|
989
1260
|
? computed(() => resolved)
|
|
990
1261
|
: computed(resolved);
|
|
@@ -1000,5 +1271,5 @@ function createTitle(fn, awaitValue = false) {
|
|
|
1000
1271
|
* Generated bundle index. Do not edit.
|
|
1001
1272
|
*/
|
|
1002
1273
|
|
|
1003
|
-
export { Link, PreloadStrategy, createBreadcrumb, createTitle, injectBreadcrumbs, injectTriggerPreload, provideBreadcrumbConfig, provideMMLinkDefaultConfig, provideTitleConfig, queryParam, url };
|
|
1274
|
+
export { Link, PreloadStrategy, createBreadcrumb, createNavItems, createTitle, injectBreadcrumbs, injectNavItems, injectTriggerPreload, provideBreadcrumbConfig, provideMMLinkDefaultConfig, provideNavConfig, provideTitleConfig, queryParam, url };
|
|
1004
1275
|
//# sourceMappingURL=mmstack-router-core.mjs.map
|