@mmstack/router-core 21.0.5 → 21.0.7

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.
@@ -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
- * @param config - A partial `BreadcrumbConfig` object with the desired settings. *
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
- * ```typescript
338
- * // In your app.module.ts or a standalone component's providers:
339
- * // import { provideBreadcrumbConfig } from './breadcrumb.config'; // Adjust path
340
- * // import { ResolvedLeafRoute } from './breadcrumb.type'; // Adjust path
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
- * // const customLabelStrategy: GenerateBreadcrumbFn = () => {
343
- * // return (leaf: ResolvedLeafRoute): string => {
344
- * // // Example: Prioritize a 'navTitle' data property
345
- * // if (leaf.route.data?.['navTitle']) {
346
- * // return leaf.route.data['navTitle'];
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
- * export const appConfig = [
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
- * This function is designed to be used as an Angular Route `ResolveFn`.
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.
521
+ *
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.
519
524
  *
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.
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
- * ```typescript
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,17 +1030,43 @@ 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
- * ```typescript
941
- * provideNavConfig({
942
- * activeMatch: { queryParams: 'ignored' },
943
- * defaults: [
944
- * { label: 'Home', link: '/' },
945
- * { label: 'Docs', link: '/docs' },
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
+ * }),
946
1055
  * ],
947
- * }),
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
+ * });
948
1070
  * ```
949
1071
  */
950
1072
  function provideNavConfig(config) {
@@ -1071,22 +1193,54 @@ function injectNavItems(name) {
1071
1193
 
1072
1194
  /**
1073
1195
  * Registers a set of nav items for the activating route under the given scope.
1074
- * Mirrors `createBreadcrumb` / `createTitle` — designed to be used in a route's
1075
- * `resolve` map.
1196
+ * Mirrors {@link createBreadcrumb} / {@link createTitle} — designed to be used
1197
+ * in a route's `resolve` map.
1076
1198
  *
1077
- * Multiple scopes can be registered on a single route by giving each its own `name`
1078
- * (and a unique key in the `resolve` map):
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 `[]`.
1079
1203
  *
1080
- * ```typescript
1081
- * resolve: {
1082
- * mainNav: createNavItems([...], { name: 'main' }),
1083
- * sideNav: createNavItems([...], { name: 'side' }),
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
+ * },
1084
1236
  * }
1085
- * ```
1086
1237
  *
1087
- * Scope override semantics: when multiple routes in the active chain register items
1088
- * under the same scope, the deepest active registration wins. Navigating away restores
1089
- * the shallower registration.
1238
+ * // Factory using inject()
1239
+ * createNavItems(() => {
1240
+ * const auth = inject(AuthStore);
1241
+ * return auth.canAdmin() ? adminItems : userItems;
1242
+ * });
1243
+ * ```
1090
1244
  */
1091
1245
  function createNavItems(itemsOrFactory, options) {
1092
1246
  const factory = typeof itemsOrFactory === 'function'
@@ -1216,7 +1370,29 @@ function queryParam(key, route = inject(ActivatedRoute)) {
1216
1370
 
1217
1371
  const token = new InjectionToken('@mmstack/router-core:title-config');
1218
1372
  /**
1219
- * used to provide the title configuration, will not be applied unless a `createTitle` resolver is used
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
+ * ```
1220
1396
  */
1221
1397
  function provideTitleConfig(config) {
1222
1398
  const prefix = config?.prefix ?? '';
@@ -1287,14 +1463,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1287
1463
  }]
1288
1464
  }], ctorParameters: () => [] });
1289
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).
1470
+ *
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.
1476
+ *
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') }
1290
1490
  *
1291
- * Creates a title resolver function that can be used in Angular's router.
1491
+ * // Factory using inject()
1492
+ * {
1493
+ * path: 'users/:id',
1494
+ * component: UserComponent,
1495
+ * title: createTitle(() => inject(ActivatedRoute).snapshot.params['id']),
1496
+ * }
1292
1497
  *
1293
- * @param factoryOrValue
1294
- * A function that returns a string or a Signal<string> representing the title or just the string directly.
1295
- * @param awaitValue
1296
- * If `true`, the resolver will wait until the title signal has a value before resolving.
1297
- * Defaults to `false`.
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
+ * ```
1298
1508
  */
1299
1509
  function createTitle(factoryOrValue, awaitValue = false) {
1300
1510
  const factory = typeof factoryOrValue === 'string' ? () => factoryOrValue : factoryOrValue;