@mmstack/router-core 21.0.3 → 21.0.5

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.
@@ -1,42 +1,13 @@
1
- import * as i0 from '@angular/core';
2
- import { inject, computed, Injectable, InjectionToken, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, linkedSignal } from '@angular/core';
3
1
  import * as i1 from '@angular/router';
4
2
  import { PRIMARY_OUTLET, EventType, Router, createUrlTreeFromSnapshot, UrlTree, RouterLink, RouterLinkWithHref, ActivatedRoute } from '@angular/router';
3
+ import * as i0 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';
8
8
  import { Subject, EMPTY, filter as filter$1, take, switchMap, finalize } from 'rxjs';
9
9
  import { Title } from '@angular/platform-browser';
10
10
 
11
- /**
12
- * @internal
13
- */
14
- const INTERNAL_BREADCRUMB_SYMBOL = Symbol.for('MMSTACK_INTERNAL_BREADCRUMB');
15
- /**
16
- * @internal
17
- */
18
- function getBreadcrumbInternals(breadcrumb) {
19
- return breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
20
- }
21
- /**
22
- * @internal
23
- */
24
- function createInternalBreadcrumb(bc, active, registered = true) {
25
- return {
26
- ...bc,
27
- [INTERNAL_BREADCRUMB_SYMBOL]: {
28
- active,
29
- registered,
30
- },
31
- };
32
- }
33
- /**
34
- * @internal
35
- */
36
- function isInternalBreadcrumb(breadcrumb) {
37
- return !!breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
38
- }
39
-
40
11
  function parsePathSegment$1(segmentString) {
41
12
  const parts = segmentString.split(';');
42
13
  const pathPart = parts[0];
@@ -252,8 +223,9 @@ function isNavigationEnd(e) {
252
223
  * }
253
224
  * ```
254
225
  */
255
- function url() {
256
- const router = inject(Router);
226
+ function url(router) {
227
+ if (!router)
228
+ router = inject(Router);
257
229
  return toSignal(router.events.pipe(filter(isNavigationEnd), map((e) => e.urlAfterRedirects)), {
258
230
  initialValue: router.url,
259
231
  });
@@ -327,7 +299,36 @@ function injectSnapshotPathResolver() {
327
299
  /**
328
300
  * @internal
329
301
  */
330
- const token$1 = new InjectionToken('MMSTACK_BREADCRUMB_CONFIG');
302
+ const INTERNAL_BREADCRUMB_SYMBOL = Symbol.for('MMSTACK_INTERNAL_BREADCRUMB');
303
+ /**
304
+ * @internal
305
+ */
306
+ function getBreadcrumbInternals(breadcrumb) {
307
+ return breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
308
+ }
309
+ /**
310
+ * @internal
311
+ */
312
+ function createInternalBreadcrumb(bc, active, registered = true) {
313
+ return {
314
+ ...bc,
315
+ [INTERNAL_BREADCRUMB_SYMBOL]: {
316
+ active,
317
+ registered,
318
+ },
319
+ };
320
+ }
321
+ /**
322
+ * @internal
323
+ */
324
+ function isInternalBreadcrumb(breadcrumb) {
325
+ return !!breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
326
+ }
327
+
328
+ /**
329
+ * @internal
330
+ */
331
+ const token$2 = new InjectionToken('@mmstack/router-core:breadcrumb-config');
331
332
  /**
332
333
  * Provides configuration for the breadcrumb system.
333
334
  * @param config - A partial `BreadcrumbConfig` object with the desired settings. *
@@ -359,7 +360,7 @@ const token$1 = new InjectionToken('MMSTACK_BREADCRUMB_CONFIG');
359
360
  */
360
361
  function provideBreadcrumbConfig(config) {
361
362
  return {
362
- provide: token$1,
363
+ provide: token$2,
363
364
  useValue: {
364
365
  ...config,
365
366
  },
@@ -369,7 +370,7 @@ function provideBreadcrumbConfig(config) {
369
370
  * @internal
370
371
  */
371
372
  function injectBreadcrumbConfig() {
372
- return (inject(token$1, {
373
+ return (inject(token$2, {
373
374
  optional: true,
374
375
  }) ?? {});
375
376
  }
@@ -515,10 +516,11 @@ function injectBreadcrumbs() {
515
516
  /**
516
517
  * Creates and registers a breadcrumb for a specific route.
517
518
  * This function is designed to be used as an Angular Route `ResolveFn`.
518
- * It handles the registration of the breadcrumb with the `BreadcrumbStore`
519
- * and ensures automatic deregistration when the route is destroyed.
520
519
  *
521
- * @param factory A function that returns a `CreateBreadcrumbOptions` object.
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
+ *
523
+ * @param factoryOrValue A static label, a static `CreateBreadcrumbOptions`, or a factory returning either.
522
524
  * @see CreateBreadcrumbOptions
523
525
  *
524
526
  * @example
@@ -528,25 +530,34 @@ function injectBreadcrumbs() {
528
530
  * path: 'home',
529
531
  * component: HomeComponent,
530
532
  * resolve: {
531
- * breadcrumb: createBreadcrumb(() => ({
532
- * label: 'Home',
533
- * });
533
+ * // shorthand for { label: 'Home' }
534
+ * breadcrumb: createBreadcrumb('Home'),
534
535
  * },
536
+ * },
537
+ * {
535
538
  * path: 'users/:userId',
536
539
  * component: UserProfileComponent,
537
540
  * resolve: {
538
541
  * breadcrumb: createBreadcrumb(() => {
539
542
  * const userStore = inject(UserStore);
540
543
  * return {
541
- * label: () => userStore.user().name ?? 'Loading...
542
- * };
543
- * })
544
+ * label: () => userStore.user().name ?? 'Loading...',
545
+ * };
546
+ * }),
544
547
  * },
545
- * }
548
+ * },
546
549
  * ];
547
550
  * ```
548
551
  */
549
- function createBreadcrumb(factory) {
552
+ function createBreadcrumb(factoryOrValue) {
553
+ const factory = typeof factoryOrValue === 'string'
554
+ ? () => ({ label: factoryOrValue })
555
+ : typeof factoryOrValue === 'function'
556
+ ? () => {
557
+ const result = factoryOrValue();
558
+ return typeof result === 'string' ? { label: result } : result;
559
+ }
560
+ : () => factoryOrValue;
550
561
  return async (route) => {
551
562
  const router = inject(Router);
552
563
  const store = inject(BreadcrumbStore);
@@ -650,6 +661,38 @@ function treeToSerializedUrl(router, urlTree) {
650
661
  return null;
651
662
  return router.serializeUrl(urlTree);
652
663
  }
664
+ /**
665
+ * Returns an imperative function that triggers preloading for an arbitrary link, using
666
+ * the same path resolution and {@link PreloadStrategy} pipeline as the {@link Link}
667
+ * (`mmLink`) directive.
668
+ *
669
+ * Use this when the `Link` directive isn't a fit — for example, preloading a route from
670
+ * an effect when a user opens a menu, hovers a non-link element, or reacts to a signal
671
+ * change — and you don't want to render an `<a [mmLink]>` just to request the preload.
672
+ *
673
+ * Requires {@link PreloadStrategy} to be wired up via `provideRouter(routes, withPreloading(PreloadStrategy))`,
674
+ * just like the directive.
675
+ *
676
+ * @returns A function accepting the same link descriptor shape as `mmLink` (`string`,
677
+ * commands array, `UrlTree`, or `null`). Passing `null` or an unresolvable link is a no-op.
678
+ *
679
+ * @example
680
+ * ```typescript
681
+ * @Component({ ... })
682
+ * export class CommandPaletteComponent {
683
+ * private readonly triggerPreload = injectTriggerPreload();
684
+ *
685
+ * protected readonly highlighted = signal<string | null>(null);
686
+ *
687
+ * constructor() {
688
+ * effect(() => {
689
+ * const target = this.highlighted();
690
+ * if (target) this.triggerPreload(target);
691
+ * });
692
+ * }
693
+ * }
694
+ * ```
695
+ */
653
696
  function injectTriggerPreload() {
654
697
  const req = inject(PreloadRequester);
655
698
  const router = inject(Router);
@@ -791,6 +834,277 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
791
834
  ]]
792
835
  }] } });
793
836
 
837
+ /** @internal */
838
+ const NEVER_TRUE = computed(() => false, ...(ngDevMode ? [{ debugName: "NEVER_TRUE" }] : /* istanbul ignore next */ []));
839
+ function wrap(value, fallback) {
840
+ if (value === undefined)
841
+ return fallback ? fallback : computed(() => undefined);
842
+ if (typeof value === 'function')
843
+ return isSignal(value) ? value : computed(value);
844
+ return computed(() => value);
845
+ }
846
+ function isAbsoluteCommandArray(commands) {
847
+ return (commands.length > 0 &&
848
+ typeof commands[0] === 'string' &&
849
+ commands[0].startsWith('/'));
850
+ }
851
+ function resolveLinkTree(input, router, relativeTo) {
852
+ const raw = typeof input === 'function' ? input() : input;
853
+ if (raw === undefined || raw === null)
854
+ return null;
855
+ if (raw instanceof UrlTree)
856
+ return raw;
857
+ if (typeof raw === 'string') {
858
+ if (raw.startsWith('/'))
859
+ return router.parseUrl(raw);
860
+ const parsed = router.parseUrl('/' + raw);
861
+ const primary = parsed.root.children['primary'];
862
+ const segments = primary ? primary.segments.map((s) => s.path) : [];
863
+ return createUrlTreeFromSnapshot(relativeTo, segments, parsed.queryParams, parsed.fragment);
864
+ }
865
+ if (isAbsoluteCommandArray(raw))
866
+ return router.createUrlTree(raw);
867
+ return createUrlTreeFromSnapshot(relativeTo, raw);
868
+ }
869
+ function resolveMeta(input) {
870
+ if (input === undefined)
871
+ return {};
872
+ return typeof input === 'function' ? input() : input;
873
+ }
874
+ /**
875
+ * @internal
876
+ * Recursively builds an {@link InternalNavItem} tree from {@link CreateNavItem} input.
877
+ * Cascades parent `disabled`/`hidden` to descendants and computes `active` against the
878
+ * current router URL using `Router.isActive`.
879
+ */
880
+ function createInternalNavItem(input, router, relativeTo, configActiveMatch, parentDisabled, parentHidden, fallbackId) {
881
+ const label = wrap(input.label);
882
+ const ariaLabel = input.ariaLabel ? wrap(input.ariaLabel) : label;
883
+ const linkTree = computed(() => resolveLinkTree(input.link, router, relativeTo), ...(ngDevMode ? [{ debugName: "linkTree" }] : /* istanbul ignore next */ []));
884
+ const link = computed(() => {
885
+ const tree = linkTree();
886
+ return tree ? router.serializeUrl(tree) : null;
887
+ }, ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
888
+ const ownDisabled = wrap(input.disabled, NEVER_TRUE);
889
+ const ownHidden = wrap(input.hidden, NEVER_TRUE);
890
+ const disabled = computed(() => parentDisabled() || ownDisabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
891
+ const hidden = computed(() => parentHidden() || ownHidden(), ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
892
+ const metaInput = input.meta;
893
+ const meta = computed(() => resolveMeta(metaInput), ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
894
+ const id = input.id !== undefined
895
+ ? wrap(input.id)
896
+ : computed(() => link() ?? fallbackId);
897
+ const childItems = (input.children ?? []).map((childInput, i) => createInternalNavItem(childInput, router, relativeTo, configActiveMatch, disabled, hidden, `${fallbackId}.${i}`));
898
+ const children = computed(() => childItems.filter((c) => !c.hidden()), ...(ngDevMode ? [{ debugName: "children" }] : /* istanbul ignore next */ []));
899
+ const mergedActiveMatch = {
900
+ ...configActiveMatch,
901
+ ...input.activeMatch,
902
+ };
903
+ const trackNavigation = url(router);
904
+ const finalOptions = {
905
+ paths: 'subset',
906
+ fragment: 'ignored',
907
+ matrixParams: 'ignored',
908
+ queryParams: 'subset',
909
+ ...mergedActiveMatch,
910
+ };
911
+ const ownActive = computed(() => {
912
+ trackNavigation();
913
+ const tree = linkTree();
914
+ return tree ? router.isActive(tree, finalOptions) : false;
915
+ }, ...(ngDevMode ? [{ debugName: "ownActive" }] : /* istanbul ignore next */ []));
916
+ const orWithChildren = input.matchesWhenChildActive ?? input.activeMatch === undefined;
917
+ const active = computed(() => ownActive() ||
918
+ (orWithChildren &&
919
+ !!childItems.length &&
920
+ childItems.some((c) => c.active())), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
921
+ return {
922
+ id,
923
+ label,
924
+ ariaLabel,
925
+ link,
926
+ active,
927
+ disabled,
928
+ hidden,
929
+ meta,
930
+ children,
931
+ };
932
+ }
933
+
934
+ /** @internal */
935
+ const token$1 = new InjectionToken('@mmstack/router-core:nav-config');
936
+ /**
937
+ * Provides global configuration for the nav system.
938
+ *
939
+ * @example
940
+ * ```typescript
941
+ * provideNavConfig({
942
+ * activeMatch: { queryParams: 'ignored' },
943
+ * defaults: [
944
+ * { label: 'Home', link: '/' },
945
+ * { label: 'Docs', link: '/docs' },
946
+ * ],
947
+ * }),
948
+ * ```
949
+ */
950
+ function provideNavConfig(config) {
951
+ const fn = typeof config === 'function' ? config : () => ({ ...config });
952
+ return {
953
+ provide: token$1,
954
+ useFactory: fn,
955
+ };
956
+ }
957
+ /** @internal */
958
+ function injectNavConfig() {
959
+ return inject(token$1, { optional: true }) ?? {};
960
+ }
961
+
962
+ /** @internal */
963
+ const DEFAULT_NAV_SCOPE = Symbol('mmstack.nav.default');
964
+ class NavStore {
965
+ map = mutable(new Map());
966
+ leafRoutes = injectLeafRoutes();
967
+ router = inject(Router);
968
+ config = injectNavConfig();
969
+ injector = inject(EnvironmentInjector);
970
+ defaultsCache = new Map();
971
+ /** @internal */
972
+ register(scope, routePath, items) {
973
+ this.map.inline((m) => {
974
+ let scopeMap = m.get(scope);
975
+ if (!scopeMap) {
976
+ scopeMap = new Map();
977
+ m.set(scope, scopeMap);
978
+ }
979
+ scopeMap.set(routePath, items);
980
+ });
981
+ }
982
+ /** @internal */
983
+ scope(name) {
984
+ return computed(() => {
985
+ const scopeMap = this.map().get(name);
986
+ if (scopeMap) {
987
+ const leaves = this.leafRoutes();
988
+ for (let i = leaves.length - 1; i >= 0; i--) {
989
+ const items = scopeMap.get(leaves[i].path);
990
+ if (items) {
991
+ return items.filter((it) => !it.hidden());
992
+ }
993
+ }
994
+ }
995
+ const defaults = this.getDefaultItems(name);
996
+ if (defaults) {
997
+ return defaults.filter((it) => !it.hidden());
998
+ }
999
+ return [];
1000
+ });
1001
+ }
1002
+ getDefaultItems(scope) {
1003
+ const cached = this.defaultsCache.get(scope);
1004
+ if (cached !== undefined)
1005
+ return cached;
1006
+ const built = this.buildDefaultItems(scope);
1007
+ this.defaultsCache.set(scope, built);
1008
+ return built;
1009
+ }
1010
+ buildDefaultItems(scope) {
1011
+ const defaults = this.config.defaults;
1012
+ if (!defaults)
1013
+ return null;
1014
+ let entry;
1015
+ if (Array.isArray(defaults) || typeof defaults === 'function') {
1016
+ if (scope === DEFAULT_NAV_SCOPE)
1017
+ entry = defaults;
1018
+ }
1019
+ else {
1020
+ const key = scope === DEFAULT_NAV_SCOPE ? '' : scope;
1021
+ entry = defaults[key];
1022
+ }
1023
+ if (!entry)
1024
+ return null;
1025
+ const resolved = entry;
1026
+ return untracked(() => runInInjectionContext(this.injector, () => {
1027
+ const inputs = typeof resolved === 'function' ? resolved() : resolved;
1028
+ const rootSnapshot = this.router.routerState.snapshot.root;
1029
+ const prefix = scope === DEFAULT_NAV_SCOPE
1030
+ ? '__defaults__'
1031
+ : `__defaults__:${scope}`;
1032
+ return inputs.map((input, i) => createInternalNavItem(input, this.router, rootSnapshot, this.config.activeMatch, NEVER_TRUE, NEVER_TRUE, `${prefix}#${i}`));
1033
+ }));
1034
+ }
1035
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1036
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, providedIn: 'root' });
1037
+ }
1038
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, decorators: [{
1039
+ type: Injectable,
1040
+ args: [{ providedIn: 'root' }]
1041
+ }] });
1042
+ /**
1043
+ * Returns a reactive list of nav items for the requested scope.
1044
+ *
1045
+ * The returned signal reflects the nearest active ancestor route that registered items
1046
+ * for `name` via `createNavItems`. If no active route has registered items for the
1047
+ * scope, falls back to `NavConfig.defaults` (when provided via `provideNavConfig`).
1048
+ * Hidden items are filtered out.
1049
+ *
1050
+ * @typeParam TMeta The shape of `NavItem.meta` for the consuming code. Untyped at the
1051
+ * registration site — this is a consumer-side assertion.
1052
+ *
1053
+ * @example
1054
+ * ```typescript
1055
+ * @Component({
1056
+ * template: `
1057
+ * @for (item of items(); track item) {
1058
+ * <a [href]="item.link()" [class.active]="item.active()">{{ item.label() }}</a>
1059
+ * }
1060
+ * `,
1061
+ * })
1062
+ * export class TopBar {
1063
+ * protected readonly items = injectNavItems();
1064
+ * }
1065
+ * ```
1066
+ */
1067
+ function injectNavItems(name) {
1068
+ const store = inject(NavStore);
1069
+ return store.scope(name ?? DEFAULT_NAV_SCOPE);
1070
+ }
1071
+
1072
+ /**
1073
+ * 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.
1076
+ *
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):
1079
+ *
1080
+ * ```typescript
1081
+ * resolve: {
1082
+ * mainNav: createNavItems([...], { name: 'main' }),
1083
+ * sideNav: createNavItems([...], { name: 'side' }),
1084
+ * }
1085
+ * ```
1086
+ *
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.
1090
+ */
1091
+ function createNavItems(itemsOrFactory, options) {
1092
+ const factory = typeof itemsOrFactory === 'function'
1093
+ ? itemsOrFactory
1094
+ : () => itemsOrFactory;
1095
+ return async (route) => {
1096
+ const router = inject(Router);
1097
+ const store = inject(NavStore);
1098
+ const resolveRoutePath = injectSnapshotPathResolver();
1099
+ const config = injectNavConfig();
1100
+ const routePath = resolveRoutePath(route);
1101
+ const scope = options?.name ?? DEFAULT_NAV_SCOPE;
1102
+ const items = factory().map((input, i) => createInternalNavItem(input, router, route, config.activeMatch, NEVER_TRUE, NEVER_TRUE, `${routePath}#${i}`));
1103
+ store.register(scope, routePath, items);
1104
+ return Promise.resolve();
1105
+ };
1106
+ }
1107
+
794
1108
  /**
795
1109
  * Creates a WritableSignal that synchronizes with a specific URL query parameter,
796
1110
  * enabling two-way binding between the signal's state and the URL.
@@ -900,7 +1214,7 @@ function queryParam(key, route = inject(ActivatedRoute)) {
900
1214
  return toWritable(queryParam, set);
901
1215
  }
902
1216
 
903
- const token = new InjectionToken('MMSTACK_TITLE_CONFIG');
1217
+ const token = new InjectionToken('@mmstack/router-core:title-config');
904
1218
  /**
905
1219
  * used to provide the title configuration, will not be applied unless a `createTitle` resolver is used
906
1220
  */
@@ -912,6 +1226,7 @@ function provideTitleConfig(config) {
912
1226
  return {
913
1227
  provide: token,
914
1228
  useValue: {
1229
+ initialTitle: config?.initialTitle ?? '',
915
1230
  parser: prefixFn,
916
1231
  keepLastKnown: config?.keepLastKnownTitle ?? true,
917
1232
  },
@@ -924,25 +1239,28 @@ function injectTitleConfig() {
924
1239
  return (inject(token, {
925
1240
  optional: true,
926
1241
  }) ?? {
1242
+ initialTitle: '',
927
1243
  parser: identity,
928
1244
  keepLastKnown: true,
929
1245
  });
930
1246
  }
931
1247
 
932
1248
  class TitleStore {
933
- title = inject(Title);
934
1249
  map = mutable(new Map());
935
- leafRoutes = injectLeafRoutes();
936
1250
  constructor() {
937
- const reverseLeaves = computed(() => this.leafRoutes().toReversed(), ...(ngDevMode ? [{ debugName: "reverseLeaves" }] : /* istanbul ignore next */ []));
1251
+ const { keepLastKnown, initialTitle } = injectTitleConfig();
1252
+ const leafRoutes = injectLeafRoutes();
1253
+ const title = inject(Title);
1254
+ const fallbackTitle = initialTitle || untracked(() => title.getTitle());
1255
+ const reverseLeaves = computed(() => leafRoutes().toReversed(), ...(ngDevMode ? [{ debugName: "reverseLeaves" }] : /* istanbul ignore next */ []));
938
1256
  const currentResolvedTitles = computed(() => {
939
1257
  const map = this.map();
940
1258
  return reverseLeaves()
941
- .map((leaf) => map.get(leaf.path)?.() ?? leaf.route.title)
942
- .filter((v) => !!v);
1259
+ .map((leaf) => map.get(leaf.path)?.() ?? leaf.route.title ?? null)
1260
+ .filter((v) => v !== null);
943
1261
  }, ...(ngDevMode ? [{ debugName: "currentResolvedTitles" }] : /* istanbul ignore next */ []));
944
1262
  const currentTitle = computed(() => currentResolvedTitles().at(0) ?? '', ...(ngDevMode ? [{ debugName: "currentTitle" }] : /* istanbul ignore next */ []));
945
- const heldTitle = injectTitleConfig().keepLastKnown
1263
+ const heldTitle = keepLastKnown
946
1264
  ? linkedSignal({
947
1265
  source: () => currentTitle(),
948
1266
  computation: (value, prev) => {
@@ -953,7 +1271,7 @@ class TitleStore {
953
1271
  })
954
1272
  : currentTitle;
955
1273
  effect(() => {
956
- this.title.setTitle(heldTitle());
1274
+ title.setTitle(heldTitle() || fallbackTitle);
957
1275
  });
958
1276
  }
959
1277
  register(id, titleFn) {
@@ -972,19 +1290,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
972
1290
  *
973
1291
  * Creates a title resolver function that can be used in Angular's router.
974
1292
  *
975
- * @param fn
976
- * A function that returns a string or a Signal<string> representing the title.
1293
+ * @param factoryOrValue
1294
+ * A function that returns a string or a Signal<string> representing the title or just the string directly.
977
1295
  * @param awaitValue
978
1296
  * If `true`, the resolver will wait until the title signal has a value before resolving.
979
1297
  * Defaults to `false`.
980
1298
  */
981
- function createTitle(fn, awaitValue = false) {
1299
+ function createTitle(factoryOrValue, awaitValue = false) {
1300
+ const factory = typeof factoryOrValue === 'string' ? () => factoryOrValue : factoryOrValue;
982
1301
  return async (route) => {
983
1302
  const store = inject(TitleStore);
984
1303
  const resolver = injectSnapshotPathResolver();
985
1304
  const fp = resolver(route);
986
1305
  const { parser } = injectTitleConfig();
987
- const resolved = fn();
1306
+ const resolved = factory();
988
1307
  const titleSignal = typeof resolved === 'string'
989
1308
  ? computed(() => resolved)
990
1309
  : computed(resolved);
@@ -1000,5 +1319,5 @@ function createTitle(fn, awaitValue = false) {
1000
1319
  * Generated bundle index. Do not edit.
1001
1320
  */
1002
1321
 
1003
- export { Link, PreloadStrategy, createBreadcrumb, createTitle, injectBreadcrumbs, injectTriggerPreload, provideBreadcrumbConfig, provideMMLinkDefaultConfig, provideTitleConfig, queryParam, url };
1322
+ export { Link, PreloadStrategy, createBreadcrumb, createNavItems, createTitle, injectBreadcrumbs, injectNavItems, injectTriggerPreload, provideBreadcrumbConfig, provideMMLinkDefaultConfig, provideNavConfig, provideTitleConfig, queryParam, url };
1004
1323
  //# sourceMappingURL=mmstack-router-core.mjs.map