@mmstack/router-core 19.3.15 → 19.3.17

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/lib/link.d.ts CHANGED
@@ -51,7 +51,56 @@ type MMLinkConfig = {
51
51
  */
52
52
  useMouseDown: boolean;
53
53
  };
54
+ /**
55
+ * Provide application-wide defaults for the `mmLink` directive. Each `[mmLink]`
56
+ * instance can still override per-link via its own `preloadOn` / `useMouseDown`
57
+ * inputs; this just shifts the default.
58
+ *
59
+ * @param config Partial override of `MMLinkConfig`. Unset keys fall back to:
60
+ * - `preloadOn: 'hover'` — preload triggered when the user hovers a link
61
+ * - `useMouseDown: false` — navigation triggered on click (not mousedown)
62
+ * @returns A `Provider` to add to your app's providers array.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * bootstrapApplication(AppComponent, {
67
+ * providers: [
68
+ * provideMMLinkDefaultConfig({ preloadOn: 'visible', useMouseDown: true }),
69
+ * ],
70
+ * });
71
+ * ```
72
+ */
54
73
  export declare function provideMMLinkDefaultConfig(config: Partial<MMLinkConfig>): Provider;
74
+ /**
75
+ * Drop-in replacement for `[routerLink]` that adds preloading on hover or
76
+ * visibility, optional mousedown-triggered navigation, and a `beforeNavigate`
77
+ * hook. Composes with Angular's `RouterLink` via `hostDirectives`, so every
78
+ * `RouterLink` input (`target`, `queryParams`, `fragment`, etc.) is forwarded.
79
+ *
80
+ * Preload behavior:
81
+ * - `preloadOn: 'hover'` (default) — preload when the user hovers the link
82
+ * - `preloadOn: 'visible'` — preload when the link scrolls into view
83
+ * - `preloadOn: null` — disable preloading on this link
84
+ *
85
+ * Navigation timing:
86
+ * - `useMouseDown: false` (default) — navigate on click
87
+ * - `useMouseDown: true` — navigate on mousedown (shaves ~50ms but breaks if the user
88
+ * moves off the link before mouseup)
89
+ *
90
+ * Requires {@link PreloadStrategy} to be wired via `provideRouter(routes, withComponentInputBinding(), withPreloading(PreloadStrategy))`.
91
+ * Set app-wide defaults with {@link provideMMLinkDefaultConfig}.
92
+ *
93
+ * @example
94
+ * ```html
95
+ * <a [mmLink]="['/users', userId()]">View profile</a>
96
+ *
97
+ * <!-- Override per-link -->
98
+ * <a [mmLink]="'/heavy-page'" preloadOn="visible" useMouseDown>Heavy page</a>
99
+ *
100
+ * <!-- React to the preload starting -->
101
+ * <a [mmLink]="'/checkout'" (preloading)="onPreload()">Checkout</a>
102
+ * ```
103
+ */
55
104
  export declare class Link {
56
105
  private readonly routerLink;
57
106
  private readonly req;
@@ -1,26 +1,74 @@
1
1
  import { type Provider } from '@angular/core';
2
2
  import { type IsActiveMatchOptions } from '@angular/router';
3
+ import { type CreateNavItem } from './nav';
4
+ /**
5
+ * Per-scope fallback descriptor: a static array or a factory returning one.
6
+ */
7
+ export type NavDefaultsForScope<TMeta = Record<string, unknown>> = CreateNavItem<TMeta>[] | (() => CreateNavItem<TMeta>[]);
3
8
  /**
4
9
  * Global configuration for the nav system.
5
10
  * @see provideNavConfig
6
11
  */
7
- export type NavConfig = {
12
+ export type NavConfig<TMeta = Record<string, unknown>> = {
8
13
  /**
9
14
  * Default match options used when computing `NavItem.active`. Per-item `activeMatch`
10
15
  * (and the router's built-in `subsetMatchOptions`) are still merged on top.
11
16
  */
12
17
  activeMatch?: Partial<IsActiveMatchOptions>;
18
+ /**
19
+ * Fallback nav items rendered when no route in the active chain has registered items
20
+ * for the requested scope. Relative `link`s resolve from `/`.
21
+ *
22
+ * Forms:
23
+ * - Array (or factory): fallback for the default (unnamed) scope.
24
+ * - Record: keys match the `name` passed to `createNavItems`. The unnamed scope can
25
+ * also be provided via this record using the empty-string key `''`.
26
+ *
27
+ * A route that wants to render an *empty* nav explicitly should register
28
+ * `createNavItems([])`, which shadows these defaults via the normal deepest-wins rule.
29
+ */
30
+ defaults?: NavDefaultsForScope<TMeta> | Record<string, NavDefaultsForScope<TMeta>>;
13
31
  };
14
32
  /**
15
- * Provides global configuration for the nav system.
33
+ * Provides global configuration for the nav system. The factory form runs in
34
+ * an injection context, so it can use `inject()` to build defaults from app
35
+ * state.
36
+ *
37
+ * @param config Either a literal {@link NavConfig} object or a factory
38
+ * `() => NavConfig`. Optional — without it, the nav system uses Angular's
39
+ * default `activeMatch` options and renders nothing in scopes that have no
40
+ * registered items.
41
+ * @returns A `Provider` to add to your app's providers array.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * bootstrapApplication(AppComponent, {
46
+ * providers: [
47
+ * provideRouter(routes),
48
+ * provideNavConfig({
49
+ * activeMatch: { queryParams: 'ignored' },
50
+ * defaults: [
51
+ * { label: 'Home', link: '/' },
52
+ * { label: 'Docs', link: '/docs' },
53
+ * ],
54
+ * }),
55
+ * ],
56
+ * });
57
+ * ```
16
58
  *
17
59
  * @example
18
- * ```typescript
19
- * provideNavConfig({
20
- * activeMatch: { queryParams: 'ignored' },
21
- * }),
60
+ * ```ts
61
+ * // Factory form — read defaults from a service
62
+ * provideNavConfig(() => {
63
+ * const role = inject(AuthStore).role();
64
+ * return {
65
+ * defaults: {
66
+ * main: role === 'admin' ? adminNav : guestNav,
67
+ * },
68
+ * };
69
+ * });
22
70
  * ```
23
71
  */
24
- export declare function provideNavConfig(config?: NavConfig): Provider;
72
+ export declare function provideNavConfig(config?: NavConfig | (() => NavConfig)): Provider;
25
73
  /** @internal */
26
74
  export declare function injectNavConfig(): NavConfig;
@@ -2,22 +2,54 @@ import { type ResolveFn } from '@angular/router';
2
2
  import { type CreateNavItem } from './nav';
3
3
  /**
4
4
  * Registers a set of nav items for the activating route under the given scope.
5
- * Mirrors `createBreadcrumb` / `createTitle` — designed to be used in a route's
6
- * `resolve` map.
5
+ * Mirrors {@link createBreadcrumb} / {@link createTitle} — designed to be used
6
+ * in a route's `resolve` map.
7
7
  *
8
- * Multiple scopes can be registered on a single route by giving each its own `name`
9
- * (and a unique key in the `resolve` map):
8
+ * Scope override semantics: when multiple routes in the active chain register
9
+ * items under the same scope, the deepest active registration wins. Navigating
10
+ * away restores the shallower registration. To explicitly render an empty nav
11
+ * (shadowing a default), pass `[]`.
10
12
  *
11
- * ```typescript
12
- * resolve: {
13
- * mainNav: createNavItems([...], { name: 'main' }),
14
- * sideNav: createNavItems([...], { name: 'side' }),
13
+ * @typeParam TMeta Optional per-item metadata type — flows through the
14
+ * registered items so consumers reading via {@link injectNavItems} get
15
+ * typed access to `item.meta`.
16
+ * @param itemsOrFactory Either a static array of {@link CreateNavItem} or a
17
+ * factory `() => CreateNavItem<TMeta>[]` invoked inside an injection
18
+ * context (so it can use `inject()` for dynamic items).
19
+ * @param options Optional `{ name }` for registering multiple scopes on a
20
+ * single route. Omit to target the default (unnamed) scope.
21
+ * @returns An Angular `ResolveFn<void>` to wire into a route's `resolve` map.
22
+ * The resolver registers items as a side effect; the resolved value itself
23
+ * is unused.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * // Single default-scope nav
28
+ * {
29
+ * path: 'app',
30
+ * resolve: {
31
+ * _nav: createNavItems([
32
+ * { label: 'Dashboard', link: 'dashboard' },
33
+ * { label: 'Reports', link: 'reports' },
34
+ * ]),
35
+ * },
15
36
  * }
16
- * ```
17
37
  *
18
- * Scope override semantics: when multiple routes in the active chain register items
19
- * under the same scope, the deepest active registration wins. Navigating away restores
20
- * the shallower registration.
38
+ * // Multiple scopes
39
+ * {
40
+ * path: 'app',
41
+ * resolve: {
42
+ * mainNav: createNavItems([...], { name: 'main' }),
43
+ * sideNav: createNavItems([...], { name: 'side' }),
44
+ * },
45
+ * }
46
+ *
47
+ * // Factory using inject()
48
+ * createNavItems(() => {
49
+ * const auth = inject(AuthStore);
50
+ * return auth.canAdmin() ? adminItems : userItems;
51
+ * });
52
+ * ```
21
53
  */
22
54
  export declare function createNavItems<TMeta = Record<string, unknown>>(itemsOrFactory: CreateNavItem<TMeta>[] | (() => CreateNavItem<TMeta>[]), options?: {
23
55
  name?: string;
@@ -8,10 +8,16 @@ type AnyInternalNavItem = InternalNavItem<unknown>;
8
8
  export declare class NavStore {
9
9
  private readonly map;
10
10
  private readonly leafRoutes;
11
+ private readonly router;
12
+ private readonly config;
13
+ private readonly injector;
14
+ private readonly defaultsCache;
11
15
  /** @internal */
12
16
  register(scope: ScopeName, routePath: string, items: AnyInternalNavItem[]): void;
13
17
  /** @internal */
14
18
  scope<TMeta = Record<string, unknown>>(name: ScopeName): Signal<NavItem<TMeta>[]>;
19
+ private getDefaultItems;
20
+ private buildDefaultItems;
15
21
  static ɵfac: i0.ɵɵFactoryDeclaration<NavStore, never>;
16
22
  static ɵprov: i0.ɵɵInjectableDeclaration<NavStore>;
17
23
  }
@@ -19,7 +25,9 @@ export declare class NavStore {
19
25
  * Returns a reactive list of nav items for the requested scope.
20
26
  *
21
27
  * The returned signal reflects the nearest active ancestor route that registered items
22
- * for `name` via `createNavItems`. Hidden items are filtered out.
28
+ * for `name` via `createNavItems`. If no active route has registered items for the
29
+ * scope, falls back to `NavConfig.defaults` (when provided via `provideNavConfig`).
30
+ * Hidden items are filtered out.
23
31
  *
24
32
  * @typeParam TMeta The shape of `NavItem.meta` for the consuming code. Untyped at the
25
33
  * registration site — this is a consumer-side assertion.
@@ -1,6 +1,44 @@
1
1
  import { PreloadingStrategy, type Route } from '@angular/router';
2
2
  import { Observable } from 'rxjs';
3
3
  import * as i0 from "@angular/core";
4
+ /**
5
+ * Demand-driven preloading strategy for Angular's router. Unlike Angular's
6
+ * built-in `PreloadAllModules`, this strategy preloads a lazy route only
7
+ * when something explicitly requests it via {@link PreloadRequester} (e.g.
8
+ * the `mmLink` directive on hover or visibility, or {@link injectTriggerPreload}
9
+ * called imperatively).
10
+ *
11
+ * Skips preloading when:
12
+ * - the route has `data.preload === false`
13
+ * - the network is on `2g` or in `saveData` mode (cheap-data-mode users)
14
+ * - a load for the same path is already in flight
15
+ *
16
+ * Wire this into `provideRouter` to enable the `mmLink` preload pipeline:
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { PreloadStrategy } from '@mmstack/router-core';
21
+ * import { provideRouter, withPreloading } from '@angular/router';
22
+ *
23
+ * bootstrapApplication(AppComponent, {
24
+ * providers: [
25
+ * provideRouter(routes, withPreloading(PreloadStrategy)),
26
+ * ],
27
+ * });
28
+ *
29
+ * // Then in templates, `mmLink` (or `injectTriggerPreload`) requests
30
+ * // preloads that this strategy executes:
31
+ * // <a [mmLink]="'/users'">Users</a>
32
+ * ```
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * // Opt a route out of preloading:
37
+ * export const routes: Routes = [
38
+ * { path: 'admin', loadChildren: () => import('./admin'), data: { preload: false } },
39
+ * ];
40
+ * ```
41
+ */
4
42
  export declare class PreloadStrategy implements PreloadingStrategy {
5
43
  private readonly loading;
6
44
  private readonly router;
@@ -32,7 +32,29 @@ export type InternalTitleConfig = {
32
32
  keepLastKnown: boolean;
33
33
  };
34
34
  /**
35
- * used to provide the title configuration, will not be applied unless a `createTitle` resolver is used
35
+ * Provide application-wide configuration for the title subsystem. The config
36
+ * is only consumed when at least one route uses a {@link createTitle} resolver;
37
+ * routes without `createTitle` are unaffected.
38
+ *
39
+ * @param config Optional {@link TitleConfig}. All fields are optional — pass
40
+ * `prefix` to namespace titles (e.g. `"My App – "`), `initialTitle` to
41
+ * override the fallback (defaults to the `<title>` from `index.html`), and
42
+ * `keepLastKnownTitle: false` to clear the title on navigations to routes
43
+ * without a title (the default keeps the previous one).
44
+ * @returns A `Provider` to add to your app's providers array.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * bootstrapApplication(AppComponent, {
49
+ * providers: [
50
+ * provideRouter(routes),
51
+ * provideTitleConfig({
52
+ * prefix: (title) => `${title} • My App`,
53
+ * keepLastKnownTitle: true,
54
+ * }),
55
+ * ],
56
+ * });
57
+ * ```
36
58
  */
37
59
  export declare function provideTitleConfig(config?: TitleConfig): Provider;
38
60
  export declare function injectTitleConfig(): InternalTitleConfig;
@@ -9,13 +9,47 @@ export declare class TitleStore {
9
9
  static ɵprov: i0.ɵɵInjectableDeclaration<TitleStore>;
10
10
  }
11
11
  /**
12
+ * Creates an Angular router `ResolveFn<string>` that registers a title for the
13
+ * route it's attached to. Titles can be static strings, factory functions
14
+ * (called in an injection context, so they can use `inject()`), or signal
15
+ * factories (for reactive titles that change when underlying data does).
12
16
  *
13
- * Creates a title resolver function that can be used in Angular's router.
17
+ * The resolved title flows through any `prefix` configured via
18
+ * {@link provideTitleConfig}, and is wired into Angular's `Title` service
19
+ * via an effect. Nested routes pick the most-specific leaf's title; if a
20
+ * deeper route has no title and `keepLastKnownTitle` is `true` (default),
21
+ * the previous title is preserved.
14
22
  *
15
- * @param factoryOrValue
16
- * A function that returns a string or a Signal<string> representing the title or just the string directly.
17
- * @param awaitValue
18
- * If `true`, the resolver will wait until the title signal has a value before resolving.
19
- * Defaults to `false`.
23
+ * @param factoryOrValue Either a literal string title, a `() => string`
24
+ * factory, or a `() => Signal<string>` factory for reactive titles. Factory
25
+ * callbacks run inside an injection context, so they can use `inject()`.
26
+ * @param awaitValue When `true`, the resolver waits until the title signal
27
+ * emits a truthy value before resolving — useful for SSR/SEO where the
28
+ * resolved title should not be empty. Defaults to `false`.
29
+ * @returns An Angular `ResolveFn<string>` to wire into a route's `title` field
30
+ * (or any other `resolve` slot — the return value isn't usually consumed).
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * // Static title
35
+ * { path: 'about', component: AboutComponent, title: createTitle('About us') }
36
+ *
37
+ * // Factory using inject()
38
+ * {
39
+ * path: 'users/:id',
40
+ * component: UserComponent,
41
+ * title: createTitle(() => inject(ActivatedRoute).snapshot.params['id']),
42
+ * }
43
+ *
44
+ * // Reactive title from a signal store
45
+ * {
46
+ * path: 'dashboard',
47
+ * component: DashboardComponent,
48
+ * title: createTitle(() => {
49
+ * const user = inject(UserStore).current;
50
+ * return computed(() => `Dashboard – ${user()?.name ?? 'Guest'}`);
51
+ * }),
52
+ * }
53
+ * ```
20
54
  */
21
55
  export declare function createTitle(factoryOrValue: (() => string | (() => string)) | string, awaitValue?: boolean): ResolveFn<string>;
@@ -2,7 +2,19 @@ import { Signal } from '@angular/core';
2
2
  import { ActivatedRouteSnapshot } from '@angular/router';
3
3
  import * as i0 from "@angular/core";
4
4
  /**
5
- * @internal
5
+ * A flattened view of one route in the active router chain, used by the
6
+ * breadcrumb and title subsystems. Each `ResolvedLeafRoute` describes one
7
+ * "step" in the chain from root to current leaf.
8
+ *
9
+ * Exposed publicly because custom breadcrumb generators (see
10
+ * {@link BreadcrumbConfig}'s `generation` callback) receive instances of
11
+ * this type and need to read its fields.
12
+ *
13
+ * - `route` — the underlying `ActivatedRouteSnapshot`.
14
+ * - `segment.path` — the route config segment (e.g. `:userId`).
15
+ * - `segment.resolved` — the resolved value of that segment (e.g. `'42'`).
16
+ * - `path` — the full route-config path from root (with raw segments like `:userId`).
17
+ * - `link` — the full resolved URL from root (with substituted values).
6
18
  */
7
19
  export type ResolvedLeafRoute = {
8
20
  route: ActivatedRouteSnapshot;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmstack/router-core",
3
- "version": "19.3.15",
3
+ "version": "19.3.17",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "signals",