@mmstack/router-core 21.0.4 → 21.0.6
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 +29 -0
- package/fesm2022/mmstack-router-core.mjs +320 -62
- package/fesm2022/mmstack-router-core.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-router-core.d.ts +303 -58
package/README.md
CHANGED
|
@@ -407,6 +407,35 @@ createNavItems(() => [
|
|
|
407
407
|
]);
|
|
408
408
|
```
|
|
409
409
|
|
|
410
|
+
#### Default (fallback) items
|
|
411
|
+
|
|
412
|
+
Routes register items on-demand, so any URL with no registration in its active chain renders an empty menu. `provideNavConfig({ defaults })` declares fallback items rendered on those URLs — handy for landing pages, error routes, or app shells where a few items should always be visible:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
provideNavConfig({
|
|
416
|
+
defaults: [
|
|
417
|
+
{ label: 'Home', link: '/' },
|
|
418
|
+
{ label: 'Docs', link: '/docs' },
|
|
419
|
+
],
|
|
420
|
+
}),
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Relative `link`s on defaults resolve from `/` (the router root), so `link: 'home'` becomes `/home`. Absolute links and `UrlTree`s pass through unchanged.
|
|
424
|
+
|
|
425
|
+
Named scopes are supported via the record form:
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
provideNavConfig({
|
|
429
|
+
defaults: {
|
|
430
|
+
'': [{ label: 'Home', link: '/' }], // default scope
|
|
431
|
+
main: [{ label: 'Home', link: '/' }], // injectNavItems('main')
|
|
432
|
+
side: () => [{ label: 'Settings', link: '/settings' }], // factory
|
|
433
|
+
},
|
|
434
|
+
}),
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Shadowing follows the usual deepest-wins rule — any active route that calls `createNavItems` (including `createNavItems([])` to render an explicitly empty menu) replaces the defaults for that scope.
|
|
438
|
+
|
|
410
439
|
#### Typed metadata
|
|
411
440
|
|
|
412
441
|
`CreateNavItem` and `NavItem` carry a `TMeta` generic so consumers can attach app-specific fields (icons, badges, etc.) without the library imposing a shape:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i1 from '@angular/router';
|
|
2
2
|
import { PRIMARY_OUTLET, EventType, Router, createUrlTreeFromSnapshot, UrlTree, RouterLink, RouterLinkWithHref, ActivatedRoute } from '@angular/router';
|
|
3
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';
|
|
4
|
+
import { inject, computed, Injectable, InjectionToken, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, EnvironmentInjector, runInInjectionContext, 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';
|
|
@@ -331,31 +331,33 @@ function isInternalBreadcrumb(breadcrumb) {
|
|
|
331
331
|
const token$2 = new InjectionToken('@mmstack/router-core:breadcrumb-config');
|
|
332
332
|
/**
|
|
333
333
|
* Provides configuration for the breadcrumb system.
|
|
334
|
-
*
|
|
334
|
+
*
|
|
335
|
+
* @param config A partial {@link BreadcrumbConfig}. The `generation` field controls
|
|
336
|
+
* automatic label generation: `'manual'` disables it (breadcrumbs only show when
|
|
337
|
+
* {@link createBreadcrumb} explicitly registers them); a function provides a
|
|
338
|
+
* custom label generator instead of the default route-title-based one.
|
|
339
|
+
* @returns A `Provider` to add to your app's providers array.
|
|
340
|
+
*
|
|
335
341
|
* @see BreadcrumbConfig
|
|
342
|
+
*
|
|
336
343
|
* @example
|
|
337
|
-
* ```
|
|
338
|
-
* //
|
|
339
|
-
*
|
|
340
|
-
*
|
|
344
|
+
* ```ts
|
|
345
|
+
* // Disable automatic generation — breadcrumbs only appear when createBreadcrumb is used
|
|
346
|
+
* bootstrapApplication(AppComponent, {
|
|
347
|
+
* providers: [
|
|
348
|
+
* provideRouter(routes),
|
|
349
|
+
* provideBreadcrumbConfig({ generation: 'manual' }),
|
|
350
|
+
* ],
|
|
351
|
+
* });
|
|
352
|
+
* ```
|
|
341
353
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* //
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* // }
|
|
348
|
-
* // // Fallback to a default mechanism
|
|
349
|
-
* // return leaf.route.title || leaf.segment.resolved || 'Unnamed';
|
|
350
|
-
* // };
|
|
351
|
-
* // };
|
|
354
|
+
* @example
|
|
355
|
+
* ```ts
|
|
356
|
+
* // Custom label strategy — outer fn runs in injection context, inner is reactive
|
|
357
|
+
* const customLabelStrategy = () => (leaf: ResolvedLeafRoute) =>
|
|
358
|
+
* leaf.route.data?.['navTitle'] ?? leaf.route.title ?? 'Unnamed';
|
|
352
359
|
*
|
|
353
|
-
*
|
|
354
|
-
* // ...rest
|
|
355
|
-
* provideBreadcrumbConfig({
|
|
356
|
-
* generation: customLabelStrategy, // or 'manual' to disable auto-generation
|
|
357
|
-
* }),
|
|
358
|
-
* ]
|
|
360
|
+
* provideBreadcrumbConfig({ generation: customLabelStrategy });
|
|
359
361
|
* ```
|
|
360
362
|
*/
|
|
361
363
|
function provideBreadcrumbConfig(config) {
|
|
@@ -514,17 +516,24 @@ function injectBreadcrumbs() {
|
|
|
514
516
|
}
|
|
515
517
|
|
|
516
518
|
/**
|
|
517
|
-
* Creates and registers a breadcrumb for a specific route.
|
|
518
|
-
*
|
|
519
|
+
* Creates and registers a breadcrumb for a specific route. Designed to be used
|
|
520
|
+
* as an Angular Route `ResolveFn` in the route's `resolve` map.
|
|
519
521
|
*
|
|
520
|
-
* Accepts a static label
|
|
521
|
-
* use a factory when you need `inject()`
|
|
522
|
+
* Accepts a static label, a static options object, or a factory returning
|
|
523
|
+
* either — use a factory when you need `inject()` to read dynamic data.
|
|
524
|
+
*
|
|
525
|
+
* @param factoryOrValue One of: a literal label string (shorthand for
|
|
526
|
+
* `{ label: <string> }`), a static {@link CreateBreadcrumbOptions} object,
|
|
527
|
+
* or a factory `() => string | CreateBreadcrumbOptions` invoked inside an
|
|
528
|
+
* injection context (so it can use `inject()`).
|
|
529
|
+
* @returns An Angular `ResolveFn<void>` to wire into a route's `resolve` map.
|
|
530
|
+
* The resolver registers the breadcrumb as a side effect; the resolved value
|
|
531
|
+
* itself is unused.
|
|
522
532
|
*
|
|
523
|
-
* @param factoryOrValue A static label, a static `CreateBreadcrumbOptions`, or a factory returning either.
|
|
524
533
|
* @see CreateBreadcrumbOptions
|
|
525
534
|
*
|
|
526
535
|
* @example
|
|
527
|
-
* ```
|
|
536
|
+
* ```ts
|
|
528
537
|
* export const appRoutes: Routes = [
|
|
529
538
|
* {
|
|
530
539
|
* path: 'home',
|
|
@@ -541,7 +550,7 @@ function injectBreadcrumbs() {
|
|
|
541
550
|
* breadcrumb: createBreadcrumb(() => {
|
|
542
551
|
* const userStore = inject(UserStore);
|
|
543
552
|
* return {
|
|
544
|
-
* label: () => userStore.user().name ?? 'Loading
|
|
553
|
+
* label: () => userStore.user().name ?? 'Loading…',
|
|
545
554
|
* };
|
|
546
555
|
* }),
|
|
547
556
|
* },
|
|
@@ -618,6 +627,44 @@ function hasSlowConnection() {
|
|
|
618
627
|
function noPreload(route) {
|
|
619
628
|
return route.data && route.data['preload'] === false;
|
|
620
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Demand-driven preloading strategy for Angular's router. Unlike Angular's
|
|
632
|
+
* built-in `PreloadAllModules`, this strategy preloads a lazy route only
|
|
633
|
+
* when something explicitly requests it via {@link PreloadRequester} (e.g.
|
|
634
|
+
* the `mmLink` directive on hover or visibility, or {@link injectTriggerPreload}
|
|
635
|
+
* called imperatively).
|
|
636
|
+
*
|
|
637
|
+
* Skips preloading when:
|
|
638
|
+
* - the route has `data.preload === false`
|
|
639
|
+
* - the network is on `2g` or in `saveData` mode (cheap-data-mode users)
|
|
640
|
+
* - a load for the same path is already in flight
|
|
641
|
+
*
|
|
642
|
+
* Wire this into `provideRouter` to enable the `mmLink` preload pipeline:
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```ts
|
|
646
|
+
* import { PreloadStrategy } from '@mmstack/router-core';
|
|
647
|
+
* import { provideRouter, withPreloading } from '@angular/router';
|
|
648
|
+
*
|
|
649
|
+
* bootstrapApplication(AppComponent, {
|
|
650
|
+
* providers: [
|
|
651
|
+
* provideRouter(routes, withPreloading(PreloadStrategy)),
|
|
652
|
+
* ],
|
|
653
|
+
* });
|
|
654
|
+
*
|
|
655
|
+
* // Then in templates, `mmLink` (or `injectTriggerPreload`) requests
|
|
656
|
+
* // preloads that this strategy executes:
|
|
657
|
+
* // <a [mmLink]="'/users'">Users</a>
|
|
658
|
+
* ```
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```ts
|
|
662
|
+
* // Opt a route out of preloading:
|
|
663
|
+
* export const routes: Routes = [
|
|
664
|
+
* { path: 'admin', loadChildren: () => import('./admin'), data: { preload: false } },
|
|
665
|
+
* ];
|
|
666
|
+
* ```
|
|
667
|
+
*/
|
|
621
668
|
class PreloadStrategy {
|
|
622
669
|
loading = new Set();
|
|
623
670
|
router = inject(Router);
|
|
@@ -705,6 +752,25 @@ function injectTriggerPreload() {
|
|
|
705
752
|
};
|
|
706
753
|
}
|
|
707
754
|
const configToken = new InjectionToken('MMSTACK_LINK_CONFIG');
|
|
755
|
+
/**
|
|
756
|
+
* Provide application-wide defaults for the `mmLink` directive. Each `[mmLink]`
|
|
757
|
+
* instance can still override per-link via its own `preloadOn` / `useMouseDown`
|
|
758
|
+
* inputs; this just shifts the default.
|
|
759
|
+
*
|
|
760
|
+
* @param config Partial override of `MMLinkConfig`. Unset keys fall back to:
|
|
761
|
+
* - `preloadOn: 'hover'` — preload triggered when the user hovers a link
|
|
762
|
+
* - `useMouseDown: false` — navigation triggered on click (not mousedown)
|
|
763
|
+
* @returns A `Provider` to add to your app's providers array.
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* ```ts
|
|
767
|
+
* bootstrapApplication(AppComponent, {
|
|
768
|
+
* providers: [
|
|
769
|
+
* provideMMLinkDefaultConfig({ preloadOn: 'visible', useMouseDown: true }),
|
|
770
|
+
* ],
|
|
771
|
+
* });
|
|
772
|
+
* ```
|
|
773
|
+
*/
|
|
708
774
|
function provideMMLinkDefaultConfig(config) {
|
|
709
775
|
const cfg = {
|
|
710
776
|
preloadOn: 'hover',
|
|
@@ -724,6 +790,36 @@ function injectConfig() {
|
|
|
724
790
|
...cfg,
|
|
725
791
|
};
|
|
726
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* Drop-in replacement for `[routerLink]` that adds preloading on hover or
|
|
795
|
+
* visibility, optional mousedown-triggered navigation, and a `beforeNavigate`
|
|
796
|
+
* hook. Composes with Angular's `RouterLink` via `hostDirectives`, so every
|
|
797
|
+
* `RouterLink` input (`target`, `queryParams`, `fragment`, etc.) is forwarded.
|
|
798
|
+
*
|
|
799
|
+
* Preload behavior:
|
|
800
|
+
* - `preloadOn: 'hover'` (default) — preload when the user hovers the link
|
|
801
|
+
* - `preloadOn: 'visible'` — preload when the link scrolls into view
|
|
802
|
+
* - `preloadOn: null` — disable preloading on this link
|
|
803
|
+
*
|
|
804
|
+
* Navigation timing:
|
|
805
|
+
* - `useMouseDown: false` (default) — navigate on click
|
|
806
|
+
* - `useMouseDown: true` — navigate on mousedown (shaves ~50ms but breaks if the user
|
|
807
|
+
* moves off the link before mouseup)
|
|
808
|
+
*
|
|
809
|
+
* Requires {@link PreloadStrategy} to be wired via `provideRouter(routes, withComponentInputBinding(), withPreloading(PreloadStrategy))`.
|
|
810
|
+
* Set app-wide defaults with {@link provideMMLinkDefaultConfig}.
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* ```html
|
|
814
|
+
* <a [mmLink]="['/users', userId()]">View profile</a>
|
|
815
|
+
*
|
|
816
|
+
* <!-- Override per-link -->
|
|
817
|
+
* <a [mmLink]="'/heavy-page'" preloadOn="visible" useMouseDown>Heavy page</a>
|
|
818
|
+
*
|
|
819
|
+
* <!-- React to the preload starting -->
|
|
820
|
+
* <a [mmLink]="'/checkout'" (preloading)="onPreload()">Checkout</a>
|
|
821
|
+
* ```
|
|
822
|
+
*/
|
|
727
823
|
class Link {
|
|
728
824
|
routerLink = inject(RouterLink, {
|
|
729
825
|
self: true,
|
|
@@ -934,19 +1030,50 @@ function createInternalNavItem(input, router, relativeTo, configActiveMatch, par
|
|
|
934
1030
|
/** @internal */
|
|
935
1031
|
const token$1 = new InjectionToken('@mmstack/router-core:nav-config');
|
|
936
1032
|
/**
|
|
937
|
-
* Provides global configuration for the nav system.
|
|
1033
|
+
* Provides global configuration for the nav system. The factory form runs in
|
|
1034
|
+
* an injection context, so it can use `inject()` to build defaults from app
|
|
1035
|
+
* state.
|
|
1036
|
+
*
|
|
1037
|
+
* @param config Either a literal {@link NavConfig} object or a factory
|
|
1038
|
+
* `() => NavConfig`. Optional — without it, the nav system uses Angular's
|
|
1039
|
+
* default `activeMatch` options and renders nothing in scopes that have no
|
|
1040
|
+
* registered items.
|
|
1041
|
+
* @returns A `Provider` to add to your app's providers array.
|
|
938
1042
|
*
|
|
939
1043
|
* @example
|
|
940
|
-
* ```
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
1044
|
+
* ```ts
|
|
1045
|
+
* bootstrapApplication(AppComponent, {
|
|
1046
|
+
* providers: [
|
|
1047
|
+
* provideRouter(routes),
|
|
1048
|
+
* provideNavConfig({
|
|
1049
|
+
* activeMatch: { queryParams: 'ignored' },
|
|
1050
|
+
* defaults: [
|
|
1051
|
+
* { label: 'Home', link: '/' },
|
|
1052
|
+
* { label: 'Docs', link: '/docs' },
|
|
1053
|
+
* ],
|
|
1054
|
+
* }),
|
|
1055
|
+
* ],
|
|
1056
|
+
* });
|
|
1057
|
+
* ```
|
|
1058
|
+
*
|
|
1059
|
+
* @example
|
|
1060
|
+
* ```ts
|
|
1061
|
+
* // Factory form — read defaults from a service
|
|
1062
|
+
* provideNavConfig(() => {
|
|
1063
|
+
* const role = inject(AuthStore).role();
|
|
1064
|
+
* return {
|
|
1065
|
+
* defaults: {
|
|
1066
|
+
* main: role === 'admin' ? adminNav : guestNav,
|
|
1067
|
+
* },
|
|
1068
|
+
* };
|
|
1069
|
+
* });
|
|
944
1070
|
* ```
|
|
945
1071
|
*/
|
|
946
1072
|
function provideNavConfig(config) {
|
|
1073
|
+
const fn = typeof config === 'function' ? config : () => ({ ...config });
|
|
947
1074
|
return {
|
|
948
1075
|
provide: token$1,
|
|
949
|
-
|
|
1076
|
+
useFactory: fn,
|
|
950
1077
|
};
|
|
951
1078
|
}
|
|
952
1079
|
/** @internal */
|
|
@@ -959,6 +1086,10 @@ const DEFAULT_NAV_SCOPE = Symbol('mmstack.nav.default');
|
|
|
959
1086
|
class NavStore {
|
|
960
1087
|
map = mutable(new Map());
|
|
961
1088
|
leafRoutes = injectLeafRoutes();
|
|
1089
|
+
router = inject(Router);
|
|
1090
|
+
config = injectNavConfig();
|
|
1091
|
+
injector = inject(EnvironmentInjector);
|
|
1092
|
+
defaultsCache = new Map();
|
|
962
1093
|
/** @internal */
|
|
963
1094
|
register(scope, routePath, items) {
|
|
964
1095
|
this.map.inline((m) => {
|
|
@@ -974,18 +1105,55 @@ class NavStore {
|
|
|
974
1105
|
scope(name) {
|
|
975
1106
|
return computed(() => {
|
|
976
1107
|
const scopeMap = this.map().get(name);
|
|
977
|
-
if (
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1108
|
+
if (scopeMap) {
|
|
1109
|
+
const leaves = this.leafRoutes();
|
|
1110
|
+
for (let i = leaves.length - 1; i >= 0; i--) {
|
|
1111
|
+
const items = scopeMap.get(leaves[i].path);
|
|
1112
|
+
if (items) {
|
|
1113
|
+
return items.filter((it) => !it.hidden());
|
|
1114
|
+
}
|
|
984
1115
|
}
|
|
985
1116
|
}
|
|
1117
|
+
const defaults = this.getDefaultItems(name);
|
|
1118
|
+
if (defaults) {
|
|
1119
|
+
return defaults.filter((it) => !it.hidden());
|
|
1120
|
+
}
|
|
986
1121
|
return [];
|
|
987
1122
|
});
|
|
988
1123
|
}
|
|
1124
|
+
getDefaultItems(scope) {
|
|
1125
|
+
const cached = this.defaultsCache.get(scope);
|
|
1126
|
+
if (cached !== undefined)
|
|
1127
|
+
return cached;
|
|
1128
|
+
const built = this.buildDefaultItems(scope);
|
|
1129
|
+
this.defaultsCache.set(scope, built);
|
|
1130
|
+
return built;
|
|
1131
|
+
}
|
|
1132
|
+
buildDefaultItems(scope) {
|
|
1133
|
+
const defaults = this.config.defaults;
|
|
1134
|
+
if (!defaults)
|
|
1135
|
+
return null;
|
|
1136
|
+
let entry;
|
|
1137
|
+
if (Array.isArray(defaults) || typeof defaults === 'function') {
|
|
1138
|
+
if (scope === DEFAULT_NAV_SCOPE)
|
|
1139
|
+
entry = defaults;
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
const key = scope === DEFAULT_NAV_SCOPE ? '' : scope;
|
|
1143
|
+
entry = defaults[key];
|
|
1144
|
+
}
|
|
1145
|
+
if (!entry)
|
|
1146
|
+
return null;
|
|
1147
|
+
const resolved = entry;
|
|
1148
|
+
return untracked(() => runInInjectionContext(this.injector, () => {
|
|
1149
|
+
const inputs = typeof resolved === 'function' ? resolved() : resolved;
|
|
1150
|
+
const rootSnapshot = this.router.routerState.snapshot.root;
|
|
1151
|
+
const prefix = scope === DEFAULT_NAV_SCOPE
|
|
1152
|
+
? '__defaults__'
|
|
1153
|
+
: `__defaults__:${scope}`;
|
|
1154
|
+
return inputs.map((input, i) => createInternalNavItem(input, this.router, rootSnapshot, this.config.activeMatch, NEVER_TRUE, NEVER_TRUE, `${prefix}#${i}`));
|
|
1155
|
+
}));
|
|
1156
|
+
}
|
|
989
1157
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
990
1158
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, providedIn: 'root' });
|
|
991
1159
|
}
|
|
@@ -997,7 +1165,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
997
1165
|
* Returns a reactive list of nav items for the requested scope.
|
|
998
1166
|
*
|
|
999
1167
|
* The returned signal reflects the nearest active ancestor route that registered items
|
|
1000
|
-
* for `name` via `createNavItems`.
|
|
1168
|
+
* for `name` via `createNavItems`. If no active route has registered items for the
|
|
1169
|
+
* scope, falls back to `NavConfig.defaults` (when provided via `provideNavConfig`).
|
|
1170
|
+
* Hidden items are filtered out.
|
|
1001
1171
|
*
|
|
1002
1172
|
* @typeParam TMeta The shape of `NavItem.meta` for the consuming code. Untyped at the
|
|
1003
1173
|
* registration site — this is a consumer-side assertion.
|
|
@@ -1023,22 +1193,54 @@ function injectNavItems(name) {
|
|
|
1023
1193
|
|
|
1024
1194
|
/**
|
|
1025
1195
|
* Registers a set of nav items for the activating route under the given scope.
|
|
1026
|
-
* Mirrors
|
|
1027
|
-
* `resolve` map.
|
|
1196
|
+
* Mirrors {@link createBreadcrumb} / {@link createTitle} — designed to be used
|
|
1197
|
+
* in a route's `resolve` map.
|
|
1028
1198
|
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1199
|
+
* Scope override semantics: when multiple routes in the active chain register
|
|
1200
|
+
* items under the same scope, the deepest active registration wins. Navigating
|
|
1201
|
+
* away restores the shallower registration. To explicitly render an empty nav
|
|
1202
|
+
* (shadowing a default), pass `[]`.
|
|
1031
1203
|
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1204
|
+
* @typeParam TMeta Optional per-item metadata type — flows through the
|
|
1205
|
+
* registered items so consumers reading via {@link injectNavItems} get
|
|
1206
|
+
* typed access to `item.meta`.
|
|
1207
|
+
* @param itemsOrFactory Either a static array of {@link CreateNavItem} or a
|
|
1208
|
+
* factory `() => CreateNavItem<TMeta>[]` invoked inside an injection
|
|
1209
|
+
* context (so it can use `inject()` for dynamic items).
|
|
1210
|
+
* @param options Optional `{ name }` for registering multiple scopes on a
|
|
1211
|
+
* single route. Omit to target the default (unnamed) scope.
|
|
1212
|
+
* @returns An Angular `ResolveFn<void>` to wire into a route's `resolve` map.
|
|
1213
|
+
* The resolver registers items as a side effect; the resolved value itself
|
|
1214
|
+
* is unused.
|
|
1215
|
+
*
|
|
1216
|
+
* @example
|
|
1217
|
+
* ```ts
|
|
1218
|
+
* // Single default-scope nav
|
|
1219
|
+
* {
|
|
1220
|
+
* path: 'app',
|
|
1221
|
+
* resolve: {
|
|
1222
|
+
* _nav: createNavItems([
|
|
1223
|
+
* { label: 'Dashboard', link: 'dashboard' },
|
|
1224
|
+
* { label: 'Reports', link: 'reports' },
|
|
1225
|
+
* ]),
|
|
1226
|
+
* },
|
|
1227
|
+
* }
|
|
1228
|
+
*
|
|
1229
|
+
* // Multiple scopes
|
|
1230
|
+
* {
|
|
1231
|
+
* path: 'app',
|
|
1232
|
+
* resolve: {
|
|
1233
|
+
* mainNav: createNavItems([...], { name: 'main' }),
|
|
1234
|
+
* sideNav: createNavItems([...], { name: 'side' }),
|
|
1235
|
+
* },
|
|
1036
1236
|
* }
|
|
1037
|
-
* ```
|
|
1038
1237
|
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1238
|
+
* // Factory using inject()
|
|
1239
|
+
* createNavItems(() => {
|
|
1240
|
+
* const auth = inject(AuthStore);
|
|
1241
|
+
* return auth.canAdmin() ? adminItems : userItems;
|
|
1242
|
+
* });
|
|
1243
|
+
* ```
|
|
1042
1244
|
*/
|
|
1043
1245
|
function createNavItems(itemsOrFactory, options) {
|
|
1044
1246
|
const factory = typeof itemsOrFactory === 'function'
|
|
@@ -1168,7 +1370,29 @@ function queryParam(key, route = inject(ActivatedRoute)) {
|
|
|
1168
1370
|
|
|
1169
1371
|
const token = new InjectionToken('@mmstack/router-core:title-config');
|
|
1170
1372
|
/**
|
|
1171
|
-
*
|
|
1373
|
+
* Provide application-wide configuration for the title subsystem. The config
|
|
1374
|
+
* is only consumed when at least one route uses a {@link createTitle} resolver;
|
|
1375
|
+
* routes without `createTitle` are unaffected.
|
|
1376
|
+
*
|
|
1377
|
+
* @param config Optional {@link TitleConfig}. All fields are optional — pass
|
|
1378
|
+
* `prefix` to namespace titles (e.g. `"My App – "`), `initialTitle` to
|
|
1379
|
+
* override the fallback (defaults to the `<title>` from `index.html`), and
|
|
1380
|
+
* `keepLastKnownTitle: false` to clear the title on navigations to routes
|
|
1381
|
+
* without a title (the default keeps the previous one).
|
|
1382
|
+
* @returns A `Provider` to add to your app's providers array.
|
|
1383
|
+
*
|
|
1384
|
+
* @example
|
|
1385
|
+
* ```ts
|
|
1386
|
+
* bootstrapApplication(AppComponent, {
|
|
1387
|
+
* providers: [
|
|
1388
|
+
* provideRouter(routes),
|
|
1389
|
+
* provideTitleConfig({
|
|
1390
|
+
* prefix: (title) => `${title} • My App`,
|
|
1391
|
+
* keepLastKnownTitle: true,
|
|
1392
|
+
* }),
|
|
1393
|
+
* ],
|
|
1394
|
+
* });
|
|
1395
|
+
* ```
|
|
1172
1396
|
*/
|
|
1173
1397
|
function provideTitleConfig(config) {
|
|
1174
1398
|
const prefix = config?.prefix ?? '';
|
|
@@ -1239,14 +1463,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
1239
1463
|
}]
|
|
1240
1464
|
}], ctorParameters: () => [] });
|
|
1241
1465
|
/**
|
|
1466
|
+
* Creates an Angular router `ResolveFn<string>` that registers a title for the
|
|
1467
|
+
* route it's attached to. Titles can be static strings, factory functions
|
|
1468
|
+
* (called in an injection context, so they can use `inject()`), or signal
|
|
1469
|
+
* factories (for reactive titles that change when underlying data does).
|
|
1242
1470
|
*
|
|
1243
|
-
*
|
|
1471
|
+
* The resolved title flows through any `prefix` configured via
|
|
1472
|
+
* {@link provideTitleConfig}, and is wired into Angular's `Title` service
|
|
1473
|
+
* via an effect. Nested routes pick the most-specific leaf's title; if a
|
|
1474
|
+
* deeper route has no title and `keepLastKnownTitle` is `true` (default),
|
|
1475
|
+
* the previous title is preserved.
|
|
1244
1476
|
*
|
|
1245
|
-
* @param factoryOrValue
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1477
|
+
* @param factoryOrValue Either a literal string title, a `() => string`
|
|
1478
|
+
* factory, or a `() => Signal<string>` factory for reactive titles. Factory
|
|
1479
|
+
* callbacks run inside an injection context, so they can use `inject()`.
|
|
1480
|
+
* @param awaitValue When `true`, the resolver waits until the title signal
|
|
1481
|
+
* emits a truthy value before resolving — useful for SSR/SEO where the
|
|
1482
|
+
* resolved title should not be empty. Defaults to `false`.
|
|
1483
|
+
* @returns An Angular `ResolveFn<string>` to wire into a route's `title` field
|
|
1484
|
+
* (or any other `resolve` slot — the return value isn't usually consumed).
|
|
1485
|
+
*
|
|
1486
|
+
* @example
|
|
1487
|
+
* ```ts
|
|
1488
|
+
* // Static title
|
|
1489
|
+
* { path: 'about', component: AboutComponent, title: createTitle('About us') }
|
|
1490
|
+
*
|
|
1491
|
+
* // Factory using inject()
|
|
1492
|
+
* {
|
|
1493
|
+
* path: 'users/:id',
|
|
1494
|
+
* component: UserComponent,
|
|
1495
|
+
* title: createTitle(() => inject(ActivatedRoute).snapshot.params['id']),
|
|
1496
|
+
* }
|
|
1497
|
+
*
|
|
1498
|
+
* // Reactive title from a signal store
|
|
1499
|
+
* {
|
|
1500
|
+
* path: 'dashboard',
|
|
1501
|
+
* component: DashboardComponent,
|
|
1502
|
+
* title: createTitle(() => {
|
|
1503
|
+
* const user = inject(UserStore).current;
|
|
1504
|
+
* return computed(() => `Dashboard – ${user()?.name ?? 'Guest'}`);
|
|
1505
|
+
* }),
|
|
1506
|
+
* }
|
|
1507
|
+
* ```
|
|
1250
1508
|
*/
|
|
1251
1509
|
function createTitle(factoryOrValue, awaitValue = false) {
|
|
1252
1510
|
const factory = typeof factoryOrValue === 'string' ? () => factoryOrValue : factoryOrValue;
|