@mmstack/router-core 20.5.3 → 20.5.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/index.d.ts CHANGED
@@ -5,7 +5,19 @@ import { Signal, InjectionToken, Provider, WritableSignal } from '@angular/core'
5
5
  import { Observable } from 'rxjs';
6
6
 
7
7
  /**
8
- * @internal
8
+ * A flattened view of one route in the active router chain, used by the
9
+ * breadcrumb and title subsystems. Each `ResolvedLeafRoute` describes one
10
+ * "step" in the chain from root to current leaf.
11
+ *
12
+ * Exposed publicly because custom breadcrumb generators (see
13
+ * {@link BreadcrumbConfig}'s `generation` callback) receive instances of
14
+ * this type and need to read its fields.
15
+ *
16
+ * - `route` — the underlying `ActivatedRouteSnapshot`.
17
+ * - `segment.path` — the route config segment (e.g. `:userId`).
18
+ * - `segment.resolved` — the resolved value of that segment (e.g. `'42'`).
19
+ * - `path` — the full route-config path from root (with raw segments like `:userId`).
20
+ * - `link` — the full resolved URL from root (with substituted values).
9
21
  */
10
22
  type ResolvedLeafRoute = {
11
23
  route: ActivatedRouteSnapshot;
@@ -78,31 +90,33 @@ type BreadcrumbConfig = {
78
90
  };
79
91
  /**
80
92
  * Provides configuration for the breadcrumb system.
81
- * @param config - A partial `BreadcrumbConfig` object with the desired settings. *
93
+ *
94
+ * @param config A partial {@link BreadcrumbConfig}. The `generation` field controls
95
+ * automatic label generation: `'manual'` disables it (breadcrumbs only show when
96
+ * {@link createBreadcrumb} explicitly registers them); a function provides a
97
+ * custom label generator instead of the default route-title-based one.
98
+ * @returns A `Provider` to add to your app's providers array.
99
+ *
82
100
  * @see BreadcrumbConfig
101
+ *
83
102
  * @example
84
- * ```typescript
85
- * // In your app.module.ts or a standalone component's providers:
86
- * // import { provideBreadcrumbConfig } from './breadcrumb.config'; // Adjust path
87
- * // import { ResolvedLeafRoute } from './breadcrumb.type'; // Adjust path
88
- *
89
- * // const customLabelStrategy: GenerateBreadcrumbFn = () => {
90
- * // return (leaf: ResolvedLeafRoute): string => {
91
- * // // Example: Prioritize a 'navTitle' data property
92
- * // if (leaf.route.data?.['navTitle']) {
93
- * // return leaf.route.data['navTitle'];
94
- * // }
95
- * // // Fallback to a default mechanism
96
- * // return leaf.route.title || leaf.segment.resolved || 'Unnamed';
97
- * // };
98
- * // };
99
- *
100
- * export const appConfig = [
101
- * // ...rest
102
- * provideBreadcrumbConfig({
103
- * generation: customLabelStrategy, // or 'manual' to disable auto-generation
104
- * }),
105
- * ]
103
+ * ```ts
104
+ * // Disable automatic generation breadcrumbs only appear when createBreadcrumb is used
105
+ * bootstrapApplication(AppComponent, {
106
+ * providers: [
107
+ * provideRouter(routes),
108
+ * provideBreadcrumbConfig({ generation: 'manual' }),
109
+ * ],
110
+ * });
111
+ * ```
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * // Custom label strategy outer fn runs in injection context, inner is reactive
116
+ * const customLabelStrategy = () => (leaf: ResolvedLeafRoute) =>
117
+ * leaf.route.data?.['navTitle'] ?? leaf.route.title ?? 'Unnamed';
118
+ *
119
+ * provideBreadcrumbConfig({ generation: customLabelStrategy });
106
120
  * ```
107
121
  */
108
122
  declare function provideBreadcrumbConfig(config: Partial<BreadcrumbConfig>): {
@@ -134,17 +148,24 @@ type CreateBreadcrumbOptions = {
134
148
  awaitValue?: boolean;
135
149
  };
136
150
  /**
137
- * Creates and registers a breadcrumb for a specific route.
138
- * This function is designed to be used as an Angular Route `ResolveFn`.
151
+ * Creates and registers a breadcrumb for a specific route. Designed to be used
152
+ * as an Angular Route `ResolveFn` in the route's `resolve` map.
153
+ *
154
+ * Accepts a static label, a static options object, or a factory returning
155
+ * either — use a factory when you need `inject()` to read dynamic data.
139
156
  *
140
- * Accepts a static label (`string`), a static options object, or a factory returning either —
141
- * use a factory when you need `inject()` for dynamic data.
157
+ * @param factoryOrValue One of: a literal label string (shorthand for
158
+ * `{ label: <string> }`), a static {@link CreateBreadcrumbOptions} object,
159
+ * or a factory `() => string | CreateBreadcrumbOptions` invoked inside an
160
+ * injection context (so it can use `inject()`).
161
+ * @returns An Angular `ResolveFn<void>` to wire into a route's `resolve` map.
162
+ * The resolver registers the breadcrumb as a side effect; the resolved value
163
+ * itself is unused.
142
164
  *
143
- * @param factoryOrValue A static label, a static `CreateBreadcrumbOptions`, or a factory returning either.
144
165
  * @see CreateBreadcrumbOptions
145
166
  *
146
167
  * @example
147
- * ```typescript
168
+ * ```ts
148
169
  * export const appRoutes: Routes = [
149
170
  * {
150
171
  * path: 'home',
@@ -161,7 +182,7 @@ type CreateBreadcrumbOptions = {
161
182
  * breadcrumb: createBreadcrumb(() => {
162
183
  * const userStore = inject(UserStore);
163
184
  * return {
164
- * label: () => userStore.user().name ?? 'Loading...',
185
+ * label: () => userStore.user().name ?? 'Loading',
165
186
  * };
166
187
  * }),
167
188
  * },
@@ -250,7 +271,56 @@ type MMLinkConfig = {
250
271
  */
251
272
  useMouseDown: boolean;
252
273
  };
274
+ /**
275
+ * Provide application-wide defaults for the `mmLink` directive. Each `[mmLink]`
276
+ * instance can still override per-link via its own `preloadOn` / `useMouseDown`
277
+ * inputs; this just shifts the default.
278
+ *
279
+ * @param config Partial override of `MMLinkConfig`. Unset keys fall back to:
280
+ * - `preloadOn: 'hover'` — preload triggered when the user hovers a link
281
+ * - `useMouseDown: false` — navigation triggered on click (not mousedown)
282
+ * @returns A `Provider` to add to your app's providers array.
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * bootstrapApplication(AppComponent, {
287
+ * providers: [
288
+ * provideMMLinkDefaultConfig({ preloadOn: 'visible', useMouseDown: true }),
289
+ * ],
290
+ * });
291
+ * ```
292
+ */
253
293
  declare function provideMMLinkDefaultConfig(config: Partial<MMLinkConfig>): Provider;
294
+ /**
295
+ * Drop-in replacement for `[routerLink]` that adds preloading on hover or
296
+ * visibility, optional mousedown-triggered navigation, and a `beforeNavigate`
297
+ * hook. Composes with Angular's `RouterLink` via `hostDirectives`, so every
298
+ * `RouterLink` input (`target`, `queryParams`, `fragment`, etc.) is forwarded.
299
+ *
300
+ * Preload behavior:
301
+ * - `preloadOn: 'hover'` (default) — preload when the user hovers the link
302
+ * - `preloadOn: 'visible'` — preload when the link scrolls into view
303
+ * - `preloadOn: null` — disable preloading on this link
304
+ *
305
+ * Navigation timing:
306
+ * - `useMouseDown: false` (default) — navigate on click
307
+ * - `useMouseDown: true` — navigate on mousedown (shaves ~50ms but breaks if the user
308
+ * moves off the link before mouseup)
309
+ *
310
+ * Requires {@link PreloadStrategy} to be wired via `provideRouter(routes, withComponentInputBinding(), withPreloading(PreloadStrategy))`.
311
+ * Set app-wide defaults with {@link provideMMLinkDefaultConfig}.
312
+ *
313
+ * @example
314
+ * ```html
315
+ * <a [mmLink]="['/users', userId()]">View profile</a>
316
+ *
317
+ * <!-- Override per-link -->
318
+ * <a [mmLink]="'/heavy-page'" preloadOn="visible" useMouseDown>Heavy page</a>
319
+ *
320
+ * <!-- React to the preload starting -->
321
+ * <a [mmLink]="'/checkout'" (preloading)="onPreload()">Checkout</a>
322
+ * ```
323
+ */
254
324
  declare class Link {
255
325
  private readonly routerLink;
256
326
  private readonly req;
@@ -384,39 +454,97 @@ type NavConfig<TMeta = Record<string, unknown>> = {
384
454
  defaults?: NavDefaultsForScope<TMeta> | Record<string, NavDefaultsForScope<TMeta>>;
385
455
  };
386
456
  /**
387
- * Provides global configuration for the nav system.
457
+ * Provides global configuration for the nav system. The factory form runs in
458
+ * an injection context, so it can use `inject()` to build defaults from app
459
+ * state.
460
+ *
461
+ * @param config Either a literal {@link NavConfig} object or a factory
462
+ * `() => NavConfig`. Optional — without it, the nav system uses Angular's
463
+ * default `activeMatch` options and renders nothing in scopes that have no
464
+ * registered items.
465
+ * @returns A `Provider` to add to your app's providers array.
388
466
  *
389
467
  * @example
390
- * ```typescript
391
- * provideNavConfig({
392
- * activeMatch: { queryParams: 'ignored' },
393
- * defaults: [
394
- * { label: 'Home', link: '/' },
395
- * { label: 'Docs', link: '/docs' },
468
+ * ```ts
469
+ * bootstrapApplication(AppComponent, {
470
+ * providers: [
471
+ * provideRouter(routes),
472
+ * provideNavConfig({
473
+ * activeMatch: { queryParams: 'ignored' },
474
+ * defaults: [
475
+ * { label: 'Home', link: '/' },
476
+ * { label: 'Docs', link: '/docs' },
477
+ * ],
478
+ * }),
396
479
  * ],
397
- * }),
480
+ * });
481
+ * ```
482
+ *
483
+ * @example
484
+ * ```ts
485
+ * // Factory form — read defaults from a service
486
+ * provideNavConfig(() => {
487
+ * const role = inject(AuthStore).role();
488
+ * return {
489
+ * defaults: {
490
+ * main: role === 'admin' ? adminNav : guestNav,
491
+ * },
492
+ * };
493
+ * });
398
494
  * ```
399
495
  */
400
496
  declare function provideNavConfig(config?: NavConfig | (() => NavConfig)): Provider;
401
497
 
402
498
  /**
403
499
  * Registers a set of nav items for the activating route under the given scope.
404
- * Mirrors `createBreadcrumb` / `createTitle` — designed to be used in a route's
405
- * `resolve` map.
500
+ * Mirrors {@link createBreadcrumb} / {@link createTitle} — designed to be used
501
+ * in a route's `resolve` map.
502
+ *
503
+ * Scope override semantics: when multiple routes in the active chain register
504
+ * items under the same scope, the deepest active registration wins. Navigating
505
+ * away restores the shallower registration. To explicitly render an empty nav
506
+ * (shadowing a default), pass `[]`.
507
+ *
508
+ * @typeParam TMeta Optional per-item metadata type — flows through the
509
+ * registered items so consumers reading via {@link injectNavItems} get
510
+ * typed access to `item.meta`.
511
+ * @param itemsOrFactory Either a static array of {@link CreateNavItem} or a
512
+ * factory `() => CreateNavItem<TMeta>[]` invoked inside an injection
513
+ * context (so it can use `inject()` for dynamic items).
514
+ * @param options Optional `{ name }` for registering multiple scopes on a
515
+ * single route. Omit to target the default (unnamed) scope.
516
+ * @returns An Angular `ResolveFn<void>` to wire into a route's `resolve` map.
517
+ * The resolver registers items as a side effect; the resolved value itself
518
+ * is unused.
406
519
  *
407
- * Multiple scopes can be registered on a single route by giving each its own `name`
408
- * (and a unique key in the `resolve` map):
520
+ * @example
521
+ * ```ts
522
+ * // Single default-scope nav
523
+ * {
524
+ * path: 'app',
525
+ * resolve: {
526
+ * _nav: createNavItems([
527
+ * { label: 'Dashboard', link: 'dashboard' },
528
+ * { label: 'Reports', link: 'reports' },
529
+ * ]),
530
+ * },
531
+ * }
409
532
  *
410
- * ```typescript
411
- * resolve: {
412
- * mainNav: createNavItems([...], { name: 'main' }),
413
- * sideNav: createNavItems([...], { name: 'side' }),
533
+ * // Multiple scopes
534
+ * {
535
+ * path: 'app',
536
+ * resolve: {
537
+ * mainNav: createNavItems([...], { name: 'main' }),
538
+ * sideNav: createNavItems([...], { name: 'side' }),
539
+ * },
414
540
  * }
415
- * ```
416
541
  *
417
- * Scope override semantics: when multiple routes in the active chain register items
418
- * under the same scope, the deepest active registration wins. Navigating away restores
419
- * the shallower registration.
542
+ * // Factory using inject()
543
+ * createNavItems(() => {
544
+ * const auth = inject(AuthStore);
545
+ * return auth.canAdmin() ? adminItems : userItems;
546
+ * });
547
+ * ```
420
548
  */
421
549
  declare function createNavItems<TMeta = Record<string, unknown>>(itemsOrFactory: CreateNavItem<TMeta>[] | (() => CreateNavItem<TMeta>[]), options?: {
422
550
  name?: string;
@@ -449,6 +577,44 @@ declare function createNavItems<TMeta = Record<string, unknown>>(itemsOrFactory:
449
577
  */
450
578
  declare function injectNavItems<TMeta = Record<string, unknown>>(name?: string): Signal<NavItem<TMeta>[]>;
451
579
 
580
+ /**
581
+ * Demand-driven preloading strategy for Angular's router. Unlike Angular's
582
+ * built-in `PreloadAllModules`, this strategy preloads a lazy route only
583
+ * when something explicitly requests it via {@link PreloadRequester} (e.g.
584
+ * the `mmLink` directive on hover or visibility, or {@link injectTriggerPreload}
585
+ * called imperatively).
586
+ *
587
+ * Skips preloading when:
588
+ * - the route has `data.preload === false`
589
+ * - the network is on `2g` or in `saveData` mode (cheap-data-mode users)
590
+ * - a load for the same path is already in flight
591
+ *
592
+ * Wire this into `provideRouter` to enable the `mmLink` preload pipeline:
593
+ *
594
+ * @example
595
+ * ```ts
596
+ * import { PreloadStrategy } from '@mmstack/router-core';
597
+ * import { provideRouter, withPreloading } from '@angular/router';
598
+ *
599
+ * bootstrapApplication(AppComponent, {
600
+ * providers: [
601
+ * provideRouter(routes, withPreloading(PreloadStrategy)),
602
+ * ],
603
+ * });
604
+ *
605
+ * // Then in templates, `mmLink` (or `injectTriggerPreload`) requests
606
+ * // preloads that this strategy executes:
607
+ * // <a [mmLink]="'/users'">Users</a>
608
+ * ```
609
+ *
610
+ * @example
611
+ * ```ts
612
+ * // Opt a route out of preloading:
613
+ * export const routes: Routes = [
614
+ * { path: 'admin', loadChildren: () => import('./admin'), data: { preload: false } },
615
+ * ];
616
+ * ```
617
+ */
452
618
  declare class PreloadStrategy implements PreloadingStrategy {
453
619
  private readonly loading;
454
620
  private readonly router;
@@ -560,19 +726,75 @@ type TitleConfig = {
560
726
  keepLastKnownTitle?: boolean;
561
727
  };
562
728
  /**
563
- * used to provide the title configuration, will not be applied unless a `createTitle` resolver is used
729
+ * Provide application-wide configuration for the title subsystem. The config
730
+ * is only consumed when at least one route uses a {@link createTitle} resolver;
731
+ * routes without `createTitle` are unaffected.
732
+ *
733
+ * @param config Optional {@link TitleConfig}. All fields are optional — pass
734
+ * `prefix` to namespace titles (e.g. `"My App – "`), `initialTitle` to
735
+ * override the fallback (defaults to the `<title>` from `index.html`), and
736
+ * `keepLastKnownTitle: false` to clear the title on navigations to routes
737
+ * without a title (the default keeps the previous one).
738
+ * @returns A `Provider` to add to your app's providers array.
739
+ *
740
+ * @example
741
+ * ```ts
742
+ * bootstrapApplication(AppComponent, {
743
+ * providers: [
744
+ * provideRouter(routes),
745
+ * provideTitleConfig({
746
+ * prefix: (title) => `${title} • My App`,
747
+ * keepLastKnownTitle: true,
748
+ * }),
749
+ * ],
750
+ * });
751
+ * ```
564
752
  */
565
753
  declare function provideTitleConfig(config?: TitleConfig): Provider;
566
754
 
567
755
  /**
756
+ * Creates an Angular router `ResolveFn<string>` that registers a title for the
757
+ * route it's attached to. Titles can be static strings, factory functions
758
+ * (called in an injection context, so they can use `inject()`), or signal
759
+ * factories (for reactive titles that change when underlying data does).
760
+ *
761
+ * The resolved title flows through any `prefix` configured via
762
+ * {@link provideTitleConfig}, and is wired into Angular's `Title` service
763
+ * via an effect. Nested routes pick the most-specific leaf's title; if a
764
+ * deeper route has no title and `keepLastKnownTitle` is `true` (default),
765
+ * the previous title is preserved.
766
+ *
767
+ * @param factoryOrValue Either a literal string title, a `() => string`
768
+ * factory, or a `() => Signal<string>` factory for reactive titles. Factory
769
+ * callbacks run inside an injection context, so they can use `inject()`.
770
+ * @param awaitValue When `true`, the resolver waits until the title signal
771
+ * emits a truthy value before resolving — useful for SSR/SEO where the
772
+ * resolved title should not be empty. Defaults to `false`.
773
+ * @returns An Angular `ResolveFn<string>` to wire into a route's `title` field
774
+ * (or any other `resolve` slot — the return value isn't usually consumed).
568
775
  *
569
- * Creates a title resolver function that can be used in Angular's router.
776
+ * @example
777
+ * ```ts
778
+ * // Static title
779
+ * { path: 'about', component: AboutComponent, title: createTitle('About us') }
780
+ *
781
+ * // Factory using inject()
782
+ * {
783
+ * path: 'users/:id',
784
+ * component: UserComponent,
785
+ * title: createTitle(() => inject(ActivatedRoute).snapshot.params['id']),
786
+ * }
570
787
  *
571
- * @param factoryOrValue
572
- * A function that returns a string or a Signal<string> representing the title or just the string directly.
573
- * @param awaitValue
574
- * If `true`, the resolver will wait until the title signal has a value before resolving.
575
- * Defaults to `false`.
788
+ * // Reactive title from a signal store
789
+ * {
790
+ * path: 'dashboard',
791
+ * component: DashboardComponent,
792
+ * title: createTitle(() => {
793
+ * const user = inject(UserStore).current;
794
+ * return computed(() => `Dashboard – ${user()?.name ?? 'Guest'}`);
795
+ * }),
796
+ * }
797
+ * ```
576
798
  */
577
799
  declare function createTitle(factoryOrValue: (() => string | (() => string)) | string, awaitValue?: boolean): ResolveFn<string>;
578
800
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmstack/router-core",
3
- "version": "20.5.3",
3
+ "version": "20.5.4",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "signals",