@mmstack/router-core 20.5.2 → 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.
139
153
  *
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.
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.
156
+ *
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;
@@ -355,47 +425,126 @@ type NavItem<TMeta = Record<string, unknown>> = {
355
425
  children: Signal<NavItem<TMeta>[]>;
356
426
  };
357
427
 
428
+ /**
429
+ * Per-scope fallback descriptor: a static array or a factory returning one.
430
+ */
431
+ type NavDefaultsForScope<TMeta = Record<string, unknown>> = CreateNavItem<TMeta>[] | (() => CreateNavItem<TMeta>[]);
358
432
  /**
359
433
  * Global configuration for the nav system.
360
434
  * @see provideNavConfig
361
435
  */
362
- type NavConfig = {
436
+ type NavConfig<TMeta = Record<string, unknown>> = {
363
437
  /**
364
438
  * Default match options used when computing `NavItem.active`. Per-item `activeMatch`
365
439
  * (and the router's built-in `subsetMatchOptions`) are still merged on top.
366
440
  */
367
441
  activeMatch?: Partial<IsActiveMatchOptions>;
442
+ /**
443
+ * Fallback nav items rendered when no route in the active chain has registered items
444
+ * for the requested scope. Relative `link`s resolve from `/`.
445
+ *
446
+ * Forms:
447
+ * - Array (or factory): fallback for the default (unnamed) scope.
448
+ * - Record: keys match the `name` passed to `createNavItems`. The unnamed scope can
449
+ * also be provided via this record using the empty-string key `''`.
450
+ *
451
+ * A route that wants to render an *empty* nav explicitly should register
452
+ * `createNavItems([])`, which shadows these defaults via the normal deepest-wins rule.
453
+ */
454
+ defaults?: NavDefaultsForScope<TMeta> | Record<string, NavDefaultsForScope<TMeta>>;
368
455
  };
369
456
  /**
370
- * 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.
371
466
  *
372
467
  * @example
373
- * ```typescript
374
- * provideNavConfig({
375
- * activeMatch: { queryParams: 'ignored' },
376
- * }),
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
+ * }),
479
+ * ],
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
+ * });
377
494
  * ```
378
495
  */
379
- declare function provideNavConfig(config?: NavConfig): Provider;
496
+ declare function provideNavConfig(config?: NavConfig | (() => NavConfig)): Provider;
380
497
 
381
498
  /**
382
499
  * Registers a set of nav items for the activating route under the given scope.
383
- * Mirrors `createBreadcrumb` / `createTitle` — designed to be used in a route's
384
- * `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.
385
519
  *
386
- * Multiple scopes can be registered on a single route by giving each its own `name`
387
- * (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
+ * }
388
532
  *
389
- * ```typescript
390
- * resolve: {
391
- * mainNav: createNavItems([...], { name: 'main' }),
392
- * sideNav: createNavItems([...], { name: 'side' }),
533
+ * // Multiple scopes
534
+ * {
535
+ * path: 'app',
536
+ * resolve: {
537
+ * mainNav: createNavItems([...], { name: 'main' }),
538
+ * sideNav: createNavItems([...], { name: 'side' }),
539
+ * },
393
540
  * }
394
- * ```
395
541
  *
396
- * Scope override semantics: when multiple routes in the active chain register items
397
- * under the same scope, the deepest active registration wins. Navigating away restores
398
- * the shallower registration.
542
+ * // Factory using inject()
543
+ * createNavItems(() => {
544
+ * const auth = inject(AuthStore);
545
+ * return auth.canAdmin() ? adminItems : userItems;
546
+ * });
547
+ * ```
399
548
  */
400
549
  declare function createNavItems<TMeta = Record<string, unknown>>(itemsOrFactory: CreateNavItem<TMeta>[] | (() => CreateNavItem<TMeta>[]), options?: {
401
550
  name?: string;
@@ -405,7 +554,9 @@ declare function createNavItems<TMeta = Record<string, unknown>>(itemsOrFactory:
405
554
  * Returns a reactive list of nav items for the requested scope.
406
555
  *
407
556
  * The returned signal reflects the nearest active ancestor route that registered items
408
- * for `name` via `createNavItems`. Hidden items are filtered out.
557
+ * for `name` via `createNavItems`. If no active route has registered items for the
558
+ * scope, falls back to `NavConfig.defaults` (when provided via `provideNavConfig`).
559
+ * Hidden items are filtered out.
409
560
  *
410
561
  * @typeParam TMeta The shape of `NavItem.meta` for the consuming code. Untyped at the
411
562
  * registration site — this is a consumer-side assertion.
@@ -426,6 +577,44 @@ declare function createNavItems<TMeta = Record<string, unknown>>(itemsOrFactory:
426
577
  */
427
578
  declare function injectNavItems<TMeta = Record<string, unknown>>(name?: string): Signal<NavItem<TMeta>[]>;
428
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
+ */
429
618
  declare class PreloadStrategy implements PreloadingStrategy {
430
619
  private readonly loading;
431
620
  private readonly router;
@@ -537,19 +726,75 @@ type TitleConfig = {
537
726
  keepLastKnownTitle?: boolean;
538
727
  };
539
728
  /**
540
- * 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
+ * ```
541
752
  */
542
753
  declare function provideTitleConfig(config?: TitleConfig): Provider;
543
754
 
544
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).
545
775
  *
546
- * 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
+ * }
547
787
  *
548
- * @param factoryOrValue
549
- * A function that returns a string or a Signal<string> representing the title or just the string directly.
550
- * @param awaitValue
551
- * If `true`, the resolver will wait until the title signal has a value before resolving.
552
- * 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
+ * ```
553
798
  */
554
799
  declare function createTitle(factoryOrValue: (() => string | (() => string)) | string, awaitValue?: boolean): ResolveFn<string>;
555
800
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmstack/router-core",
3
- "version": "20.5.2",
3
+ "version": "20.5.4",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "signals",