@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/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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, computed, Injectable, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, linkedSignal } from '@angular/core';
2
+ import { InjectionToken, inject, computed, Injectable, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, EnvironmentInjector, runInInjectionContext, linkedSignal } from '@angular/core';
3
3
  import * as i1 from '@angular/router';
4
4
  import { EventType, Router, PRIMARY_OUTLET, createUrlTreeFromSnapshot, UrlTree, RouterLink, RouterLinkWithHref, ActivatedRoute } from '@angular/router';
5
5
  import { mutable, mapArray, until, elementVisibility, toWritable } from '@mmstack/primitives';
@@ -14,31 +14,33 @@ import { Title } from '@angular/platform-browser';
14
14
  const token$2 = new InjectionToken('@mmstack/router-core:breadcrumb-config');
15
15
  /**
16
16
  * Provides configuration for the breadcrumb system.
17
- * @param config - A partial `BreadcrumbConfig` object with the desired settings. *
17
+ *
18
+ * @param config A partial {@link BreadcrumbConfig}. The `generation` field controls
19
+ * automatic label generation: `'manual'` disables it (breadcrumbs only show when
20
+ * {@link createBreadcrumb} explicitly registers them); a function provides a
21
+ * custom label generator instead of the default route-title-based one.
22
+ * @returns A `Provider` to add to your app's providers array.
23
+ *
18
24
  * @see BreadcrumbConfig
25
+ *
19
26
  * @example
20
- * ```typescript
21
- * // In your app.module.ts or a standalone component's providers:
22
- * // import { provideBreadcrumbConfig } from './breadcrumb.config'; // Adjust path
23
- * // import { ResolvedLeafRoute } from './breadcrumb.type'; // Adjust path
27
+ * ```ts
28
+ * // Disable automatic generation breadcrumbs only appear when createBreadcrumb is used
29
+ * bootstrapApplication(AppComponent, {
30
+ * providers: [
31
+ * provideRouter(routes),
32
+ * provideBreadcrumbConfig({ generation: 'manual' }),
33
+ * ],
34
+ * });
35
+ * ```
24
36
  *
25
- * // const customLabelStrategy: GenerateBreadcrumbFn = () => {
26
- * // return (leaf: ResolvedLeafRoute): string => {
27
- * // // Example: Prioritize a 'navTitle' data property
28
- * // if (leaf.route.data?.['navTitle']) {
29
- * // return leaf.route.data['navTitle'];
30
- * // }
31
- * // // Fallback to a default mechanism
32
- * // return leaf.route.title || leaf.segment.resolved || 'Unnamed';
33
- * // };
34
- * // };
37
+ * @example
38
+ * ```ts
39
+ * // Custom label strategy outer fn runs in injection context, inner is reactive
40
+ * const customLabelStrategy = () => (leaf: ResolvedLeafRoute) =>
41
+ * leaf.route.data?.['navTitle'] ?? leaf.route.title ?? 'Unnamed';
35
42
  *
36
- * export const appConfig = [
37
- * // ...rest
38
- * provideBreadcrumbConfig({
39
- * generation: customLabelStrategy, // or 'manual' to disable auto-generation
40
- * }),
41
- * ]
43
+ * provideBreadcrumbConfig({ generation: customLabelStrategy });
42
44
  * ```
43
45
  */
44
46
  function provideBreadcrumbConfig(config) {
@@ -514,17 +516,24 @@ function injectSnapshotPathResolver() {
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.
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
+ * 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
- * ```typescript
536
+ * ```ts
528
537
  * export const appRoutes: Routes = [
529
538
  * {
530
539
  * path: 'home',
@@ -541,7 +550,7 @@ function injectSnapshotPathResolver() {
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,
@@ -839,19 +935,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
839
935
  /** @internal */
840
936
  const token$1 = new InjectionToken('@mmstack/router-core:nav-config');
841
937
  /**
842
- * Provides global configuration for the nav system.
938
+ * Provides global configuration for the nav system. The factory form runs in
939
+ * an injection context, so it can use `inject()` to build defaults from app
940
+ * state.
941
+ *
942
+ * @param config Either a literal {@link NavConfig} object or a factory
943
+ * `() => NavConfig`. Optional — without it, the nav system uses Angular's
944
+ * default `activeMatch` options and renders nothing in scopes that have no
945
+ * registered items.
946
+ * @returns A `Provider` to add to your app's providers array.
843
947
  *
844
948
  * @example
845
- * ```typescript
846
- * provideNavConfig({
847
- * activeMatch: { queryParams: 'ignored' },
848
- * }),
949
+ * ```ts
950
+ * bootstrapApplication(AppComponent, {
951
+ * providers: [
952
+ * provideRouter(routes),
953
+ * provideNavConfig({
954
+ * activeMatch: { queryParams: 'ignored' },
955
+ * defaults: [
956
+ * { label: 'Home', link: '/' },
957
+ * { label: 'Docs', link: '/docs' },
958
+ * ],
959
+ * }),
960
+ * ],
961
+ * });
962
+ * ```
963
+ *
964
+ * @example
965
+ * ```ts
966
+ * // Factory form — read defaults from a service
967
+ * provideNavConfig(() => {
968
+ * const role = inject(AuthStore).role();
969
+ * return {
970
+ * defaults: {
971
+ * main: role === 'admin' ? adminNav : guestNav,
972
+ * },
973
+ * };
974
+ * });
849
975
  * ```
850
976
  */
851
977
  function provideNavConfig(config) {
978
+ const fn = typeof config === 'function' ? config : () => ({ ...config });
852
979
  return {
853
980
  provide: token$1,
854
- useValue: { ...config },
981
+ useFactory: fn,
855
982
  };
856
983
  }
857
984
  /** @internal */
@@ -961,6 +1088,10 @@ const DEFAULT_NAV_SCOPE = Symbol('mmstack.nav.default');
961
1088
  class NavStore {
962
1089
  map = mutable(new Map());
963
1090
  leafRoutes = injectLeafRoutes();
1091
+ router = inject(Router);
1092
+ config = injectNavConfig();
1093
+ injector = inject(EnvironmentInjector);
1094
+ defaultsCache = new Map();
964
1095
  /** @internal */
965
1096
  register(scope, routePath, items) {
966
1097
  this.map.inline((m) => {
@@ -976,18 +1107,55 @@ class NavStore {
976
1107
  scope(name) {
977
1108
  return computed(() => {
978
1109
  const scopeMap = this.map().get(name);
979
- if (!scopeMap)
980
- return [];
981
- const leaves = this.leafRoutes();
982
- for (let i = leaves.length - 1; i >= 0; i--) {
983
- const items = scopeMap.get(leaves[i].path);
984
- if (items) {
985
- return items.filter((it) => !it.hidden());
1110
+ if (scopeMap) {
1111
+ const leaves = this.leafRoutes();
1112
+ for (let i = leaves.length - 1; i >= 0; i--) {
1113
+ const items = scopeMap.get(leaves[i].path);
1114
+ if (items) {
1115
+ return items.filter((it) => !it.hidden());
1116
+ }
986
1117
  }
987
1118
  }
1119
+ const defaults = this.getDefaultItems(name);
1120
+ if (defaults) {
1121
+ return defaults.filter((it) => !it.hidden());
1122
+ }
988
1123
  return [];
989
1124
  });
990
1125
  }
1126
+ getDefaultItems(scope) {
1127
+ const cached = this.defaultsCache.get(scope);
1128
+ if (cached !== undefined)
1129
+ return cached;
1130
+ const built = this.buildDefaultItems(scope);
1131
+ this.defaultsCache.set(scope, built);
1132
+ return built;
1133
+ }
1134
+ buildDefaultItems(scope) {
1135
+ const defaults = this.config.defaults;
1136
+ if (!defaults)
1137
+ return null;
1138
+ let entry;
1139
+ if (Array.isArray(defaults) || typeof defaults === 'function') {
1140
+ if (scope === DEFAULT_NAV_SCOPE)
1141
+ entry = defaults;
1142
+ }
1143
+ else {
1144
+ const key = scope === DEFAULT_NAV_SCOPE ? '' : scope;
1145
+ entry = defaults[key];
1146
+ }
1147
+ if (!entry)
1148
+ return null;
1149
+ const resolved = entry;
1150
+ return untracked(() => runInInjectionContext(this.injector, () => {
1151
+ const inputs = typeof resolved === 'function' ? resolved() : resolved;
1152
+ const rootSnapshot = this.router.routerState.snapshot.root;
1153
+ const prefix = scope === DEFAULT_NAV_SCOPE
1154
+ ? '__defaults__'
1155
+ : `__defaults__:${scope}`;
1156
+ return inputs.map((input, i) => createInternalNavItem(input, this.router, rootSnapshot, this.config.activeMatch, NEVER_TRUE, NEVER_TRUE, `${prefix}#${i}`));
1157
+ }));
1158
+ }
991
1159
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: NavStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
992
1160
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: NavStore, providedIn: 'root' });
993
1161
  }
@@ -999,7 +1167,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
999
1167
  * Returns a reactive list of nav items for the requested scope.
1000
1168
  *
1001
1169
  * The returned signal reflects the nearest active ancestor route that registered items
1002
- * for `name` via `createNavItems`. Hidden items are filtered out.
1170
+ * for `name` via `createNavItems`. If no active route has registered items for the
1171
+ * scope, falls back to `NavConfig.defaults` (when provided via `provideNavConfig`).
1172
+ * Hidden items are filtered out.
1003
1173
  *
1004
1174
  * @typeParam TMeta The shape of `NavItem.meta` for the consuming code. Untyped at the
1005
1175
  * registration site — this is a consumer-side assertion.
@@ -1025,22 +1195,54 @@ function injectNavItems(name) {
1025
1195
 
1026
1196
  /**
1027
1197
  * Registers a set of nav items for the activating route under the given scope.
1028
- * Mirrors `createBreadcrumb` / `createTitle` — designed to be used in a route's
1029
- * `resolve` map.
1198
+ * Mirrors {@link createBreadcrumb} / {@link createTitle} — designed to be used
1199
+ * in a route's `resolve` map.
1030
1200
  *
1031
- * Multiple scopes can be registered on a single route by giving each its own `name`
1032
- * (and a unique key in the `resolve` map):
1201
+ * Scope override semantics: when multiple routes in the active chain register
1202
+ * items under the same scope, the deepest active registration wins. Navigating
1203
+ * away restores the shallower registration. To explicitly render an empty nav
1204
+ * (shadowing a default), pass `[]`.
1033
1205
  *
1034
- * ```typescript
1035
- * resolve: {
1036
- * mainNav: createNavItems([...], { name: 'main' }),
1037
- * sideNav: createNavItems([...], { name: 'side' }),
1206
+ * @typeParam TMeta Optional per-item metadata type — flows through the
1207
+ * registered items so consumers reading via {@link injectNavItems} get
1208
+ * typed access to `item.meta`.
1209
+ * @param itemsOrFactory Either a static array of {@link CreateNavItem} or a
1210
+ * factory `() => CreateNavItem<TMeta>[]` invoked inside an injection
1211
+ * context (so it can use `inject()` for dynamic items).
1212
+ * @param options Optional `{ name }` for registering multiple scopes on a
1213
+ * single route. Omit to target the default (unnamed) scope.
1214
+ * @returns An Angular `ResolveFn<void>` to wire into a route's `resolve` map.
1215
+ * The resolver registers items as a side effect; the resolved value itself
1216
+ * is unused.
1217
+ *
1218
+ * @example
1219
+ * ```ts
1220
+ * // Single default-scope nav
1221
+ * {
1222
+ * path: 'app',
1223
+ * resolve: {
1224
+ * _nav: createNavItems([
1225
+ * { label: 'Dashboard', link: 'dashboard' },
1226
+ * { label: 'Reports', link: 'reports' },
1227
+ * ]),
1228
+ * },
1229
+ * }
1230
+ *
1231
+ * // Multiple scopes
1232
+ * {
1233
+ * path: 'app',
1234
+ * resolve: {
1235
+ * mainNav: createNavItems([...], { name: 'main' }),
1236
+ * sideNav: createNavItems([...], { name: 'side' }),
1237
+ * },
1038
1238
  * }
1039
- * ```
1040
1239
  *
1041
- * Scope override semantics: when multiple routes in the active chain register items
1042
- * under the same scope, the deepest active registration wins. Navigating away restores
1043
- * the shallower registration.
1240
+ * // Factory using inject()
1241
+ * createNavItems(() => {
1242
+ * const auth = inject(AuthStore);
1243
+ * return auth.canAdmin() ? adminItems : userItems;
1244
+ * });
1245
+ * ```
1044
1246
  */
1045
1247
  function createNavItems(itemsOrFactory, options) {
1046
1248
  const factory = typeof itemsOrFactory === 'function'
@@ -1170,7 +1372,29 @@ function queryParam(key, route = inject(ActivatedRoute)) {
1170
1372
 
1171
1373
  const token = new InjectionToken('@mmstack/router-core:title-config');
1172
1374
  /**
1173
- * used to provide the title configuration, will not be applied unless a `createTitle` resolver is used
1375
+ * Provide application-wide configuration for the title subsystem. The config
1376
+ * is only consumed when at least one route uses a {@link createTitle} resolver;
1377
+ * routes without `createTitle` are unaffected.
1378
+ *
1379
+ * @param config Optional {@link TitleConfig}. All fields are optional — pass
1380
+ * `prefix` to namespace titles (e.g. `"My App – "`), `initialTitle` to
1381
+ * override the fallback (defaults to the `<title>` from `index.html`), and
1382
+ * `keepLastKnownTitle: false` to clear the title on navigations to routes
1383
+ * without a title (the default keeps the previous one).
1384
+ * @returns A `Provider` to add to your app's providers array.
1385
+ *
1386
+ * @example
1387
+ * ```ts
1388
+ * bootstrapApplication(AppComponent, {
1389
+ * providers: [
1390
+ * provideRouter(routes),
1391
+ * provideTitleConfig({
1392
+ * prefix: (title) => `${title} • My App`,
1393
+ * keepLastKnownTitle: true,
1394
+ * }),
1395
+ * ],
1396
+ * });
1397
+ * ```
1174
1398
  */
1175
1399
  function provideTitleConfig(config) {
1176
1400
  const prefix = config?.prefix ?? '';
@@ -1241,14 +1465,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1241
1465
  }]
1242
1466
  }], ctorParameters: () => [] });
1243
1467
  /**
1468
+ * Creates an Angular router `ResolveFn<string>` that registers a title for the
1469
+ * route it's attached to. Titles can be static strings, factory functions
1470
+ * (called in an injection context, so they can use `inject()`), or signal
1471
+ * factories (for reactive titles that change when underlying data does).
1244
1472
  *
1245
- * Creates a title resolver function that can be used in Angular's router.
1473
+ * The resolved title flows through any `prefix` configured via
1474
+ * {@link provideTitleConfig}, and is wired into Angular's `Title` service
1475
+ * via an effect. Nested routes pick the most-specific leaf's title; if a
1476
+ * deeper route has no title and `keepLastKnownTitle` is `true` (default),
1477
+ * the previous title is preserved.
1246
1478
  *
1247
- * @param factoryOrValue
1248
- * A function that returns a string or a Signal<string> representing the title or just the string directly.
1249
- * @param awaitValue
1250
- * If `true`, the resolver will wait until the title signal has a value before resolving.
1251
- * Defaults to `false`.
1479
+ * @param factoryOrValue Either a literal string title, a `() => string`
1480
+ * factory, or a `() => Signal<string>` factory for reactive titles. Factory
1481
+ * callbacks run inside an injection context, so they can use `inject()`.
1482
+ * @param awaitValue When `true`, the resolver waits until the title signal
1483
+ * emits a truthy value before resolving — useful for SSR/SEO where the
1484
+ * resolved title should not be empty. Defaults to `false`.
1485
+ * @returns An Angular `ResolveFn<string>` to wire into a route's `title` field
1486
+ * (or any other `resolve` slot — the return value isn't usually consumed).
1487
+ *
1488
+ * @example
1489
+ * ```ts
1490
+ * // Static title
1491
+ * { path: 'about', component: AboutComponent, title: createTitle('About us') }
1492
+ *
1493
+ * // Factory using inject()
1494
+ * {
1495
+ * path: 'users/:id',
1496
+ * component: UserComponent,
1497
+ * title: createTitle(() => inject(ActivatedRoute).snapshot.params['id']),
1498
+ * }
1499
+ *
1500
+ * // Reactive title from a signal store
1501
+ * {
1502
+ * path: 'dashboard',
1503
+ * component: DashboardComponent,
1504
+ * title: createTitle(() => {
1505
+ * const user = inject(UserStore).current;
1506
+ * return computed(() => `Dashboard – ${user()?.name ?? 'Guest'}`);
1507
+ * }),
1508
+ * }
1509
+ * ```
1252
1510
  */
1253
1511
  function createTitle(factoryOrValue, awaitValue = false) {
1254
1512
  const factory = typeof factoryOrValue === 'string' ? () => factoryOrValue : factoryOrValue;