@mmstack/router-core 19.3.16 → 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/fesm2022/mmstack-router-core.mjs +265 -55
- package/fesm2022/mmstack-router-core.mjs.map +1 -1
- package/lib/breadcrumb/breadcrumb-config.d.ts +23 -21
- package/lib/breadcrumb/breadcrumb-resolver.d.ts +14 -7
- package/lib/link.d.ts +49 -0
- package/lib/nav/nav-config.d.ts +34 -8
- package/lib/nav/nav-resolver.d.ts +44 -12
- package/lib/preloading/preload-strategy.d.ts +38 -0
- package/lib/title/title-config.d.ts +23 -1
- package/lib/title/title-store.d.ts +40 -6
- package/lib/util/leaf.store.d.ts +13 -1
- package/package.json +1 -1
|
@@ -21,17 +21,24 @@ type CreateBreadcrumbOptions = {
|
|
|
21
21
|
awaitValue?: boolean;
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
* Creates and registers a breadcrumb for a specific route.
|
|
25
|
-
*
|
|
24
|
+
* Creates and registers a breadcrumb for a specific route. Designed to be used
|
|
25
|
+
* as an Angular Route `ResolveFn` in the route's `resolve` map.
|
|
26
26
|
*
|
|
27
|
-
* Accepts a static label
|
|
28
|
-
* use a factory when you need `inject()`
|
|
27
|
+
* Accepts a static label, a static options object, or a factory returning
|
|
28
|
+
* either — use a factory when you need `inject()` to read dynamic data.
|
|
29
|
+
*
|
|
30
|
+
* @param factoryOrValue One of: a literal label string (shorthand for
|
|
31
|
+
* `{ label: <string> }`), a static {@link CreateBreadcrumbOptions} object,
|
|
32
|
+
* or a factory `() => string | CreateBreadcrumbOptions` invoked inside an
|
|
33
|
+
* injection context (so it can use `inject()`).
|
|
34
|
+
* @returns An Angular `ResolveFn<void>` to wire into a route's `resolve` map.
|
|
35
|
+
* The resolver registers the breadcrumb as a side effect; the resolved value
|
|
36
|
+
* itself is unused.
|
|
29
37
|
*
|
|
30
|
-
* @param factoryOrValue A static label, a static `CreateBreadcrumbOptions`, or a factory returning either.
|
|
31
38
|
* @see CreateBreadcrumbOptions
|
|
32
39
|
*
|
|
33
40
|
* @example
|
|
34
|
-
* ```
|
|
41
|
+
* ```ts
|
|
35
42
|
* export const appRoutes: Routes = [
|
|
36
43
|
* {
|
|
37
44
|
* path: 'home',
|
|
@@ -48,7 +55,7 @@ type CreateBreadcrumbOptions = {
|
|
|
48
55
|
* breadcrumb: createBreadcrumb(() => {
|
|
49
56
|
* const userStore = inject(UserStore);
|
|
50
57
|
* return {
|
|
51
|
-
* label: () => userStore.user().name ?? 'Loading
|
|
58
|
+
* label: () => userStore.user().name ?? 'Loading…',
|
|
52
59
|
* };
|
|
53
60
|
* }),
|
|
54
61
|
* },
|
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;
|
package/lib/nav/nav-config.d.ts
CHANGED
|
@@ -30,17 +30,43 @@ export type NavConfig<TMeta = Record<string, unknown>> = {
|
|
|
30
30
|
defaults?: NavDefaultsForScope<TMeta> | Record<string, NavDefaultsForScope<TMeta>>;
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
|
-
* 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.
|
|
34
42
|
*
|
|
35
43
|
* @example
|
|
36
|
-
* ```
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* {
|
|
41
|
-
*
|
|
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
|
+
* }),
|
|
42
55
|
* ],
|
|
43
|
-
* })
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
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
|
+
* });
|
|
44
70
|
* ```
|
|
45
71
|
*/
|
|
46
72
|
export declare function provideNavConfig(config?: NavConfig | (() => NavConfig)): Provider;
|
|
@@ -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
|
|
6
|
-
* `resolve` map.
|
|
5
|
+
* Mirrors {@link createBreadcrumb} / {@link createTitle} — designed to be used
|
|
6
|
+
* in a route's `resolve` map.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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;
|
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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>;
|
package/lib/util/leaf.store.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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;
|