@mmstack/router-core 21.0.2 → 21.0.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 +352 -206
- package/fesm2022/mmstack-router-core.mjs +692 -414
- package/fesm2022/mmstack-router-core.mjs.map +1 -1
- package/package.json +5 -3
- package/types/mmstack-router-core.d.ts +218 -34
|
@@ -1,63 +1,191 @@
|
|
|
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';
|
|
3
1
|
import * as i1 from '@angular/router';
|
|
4
|
-
import { EventType, Router,
|
|
5
|
-
import
|
|
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, linkedSignal } from '@angular/core';
|
|
6
5
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
7
6
|
import { filter, map } from 'rxjs/operators';
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
11
|
+
function parsePathSegment$1(segmentString) {
|
|
12
|
+
const parts = segmentString.split(';');
|
|
13
|
+
const pathPart = parts[0];
|
|
14
|
+
const matrixParams = {};
|
|
15
|
+
for (let i = 1; i < parts.length; i++) {
|
|
16
|
+
const [key, value = 'true'] = parts[i].split('=');
|
|
17
|
+
if (key) {
|
|
18
|
+
matrixParams[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { pathPart, matrixParams };
|
|
22
|
+
}
|
|
23
|
+
function createBasePredicate(path) {
|
|
24
|
+
const partPredicates = path
|
|
25
|
+
.split('/')
|
|
26
|
+
.filter((part) => !!part.trim())
|
|
27
|
+
.map((configSegmentString) => {
|
|
28
|
+
const { pathPart: configPathPart, matrixParams: configMatrixParams } = parsePathSegment$1(configSegmentString);
|
|
29
|
+
let singlePathPartPredicate;
|
|
30
|
+
if (configPathPart.startsWith(':')) {
|
|
31
|
+
singlePathPartPredicate = () => true;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
singlePathPartPredicate = (linkSegmentPathPart) => linkSegmentPathPart === configPathPart;
|
|
35
|
+
}
|
|
36
|
+
const configSegmentHasMatrixParams = Object.keys(configMatrixParams).length > 0;
|
|
37
|
+
return (linkSegmentString) => {
|
|
38
|
+
const { pathPart: linkPathPart, matrixParams: linkMatrixParams } = parsePathSegment$1(linkSegmentString);
|
|
39
|
+
if (!singlePathPartPredicate(linkPathPart)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (!configSegmentHasMatrixParams) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return Object.entries(configMatrixParams).every(([key, value]) => Object.prototype.hasOwnProperty.call(linkMatrixParams, key) &&
|
|
46
|
+
linkMatrixParams[key] === value);
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
return (path) => {
|
|
50
|
+
const linkPathOnly = path.split(/[?#]/).at(0) ?? '';
|
|
51
|
+
if (!linkPathOnly && partPredicates.length > 0)
|
|
52
|
+
return false;
|
|
53
|
+
if (!linkPathOnly && partPredicates.length === 0)
|
|
54
|
+
return true;
|
|
55
|
+
const parts = linkPathOnly.split('/').filter((part) => !!part.trim());
|
|
56
|
+
if (parts.length < partPredicates.length)
|
|
57
|
+
return false;
|
|
58
|
+
return parts.every((seg, idx) => {
|
|
59
|
+
const pred = partPredicates.at(idx);
|
|
60
|
+
if (!pred)
|
|
61
|
+
return true;
|
|
62
|
+
return pred(seg);
|
|
63
|
+
});
|
|
50
64
|
};
|
|
51
65
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
66
|
+
function singleSegmentMatches(configSegment, linkSegment) {
|
|
67
|
+
if (configSegment.pathPart === ':') {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
else if (configSegment.pathPart !== linkSegment.pathPart) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const configMatrix = configSegment.matrixParams;
|
|
74
|
+
const linkMatrix = linkSegment.matrixParams;
|
|
75
|
+
for (const key in configMatrix) {
|
|
76
|
+
if (!Object.prototype.hasOwnProperty.call(linkMatrix, key) ||
|
|
77
|
+
linkMatrix[key] !== configMatrix[key]) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
function matchSegmentsRecursive(configSegments, linkSegments, configIdx, linkIdx) {
|
|
84
|
+
if (configIdx === configSegments.length) {
|
|
85
|
+
return linkIdx === linkSegments.length;
|
|
86
|
+
}
|
|
87
|
+
if (linkIdx === linkSegments.length) {
|
|
88
|
+
for (let i = configIdx; i < configSegments.length; i++) {
|
|
89
|
+
if (configSegments[i].pathPart !== '**') {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const currentConfigSegment = configSegments[configIdx];
|
|
96
|
+
if (currentConfigSegment.pathPart === '**') {
|
|
97
|
+
if (matchSegmentsRecursive(configSegments, linkSegments, configIdx + 1, linkIdx)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (linkIdx < linkSegments.length) {
|
|
101
|
+
if (matchSegmentsRecursive(configSegments, linkSegments, configIdx, linkIdx + 1)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (linkIdx < linkSegments.length &&
|
|
109
|
+
singleSegmentMatches(currentConfigSegment, linkSegments[linkIdx])) {
|
|
110
|
+
return matchSegmentsRecursive(configSegments, linkSegments, configIdx + 1, linkIdx + 1);
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function createWildcardPredicate(path) {
|
|
116
|
+
const configSegments = path
|
|
117
|
+
.split('/')
|
|
118
|
+
.filter((p) => !!p.trim())
|
|
119
|
+
.map((segment) => parsePathSegment$1(segment));
|
|
120
|
+
return (linkPath) => {
|
|
121
|
+
const linkPathOnly = linkPath.split(/[?#]/).at(0) ?? '';
|
|
122
|
+
const linkSegments = linkPathOnly
|
|
123
|
+
.split('/')
|
|
124
|
+
.filter((p) => !!p.trim())
|
|
125
|
+
.map((segment) => parsePathSegment$1(segment));
|
|
126
|
+
return matchSegmentsRecursive(configSegments, linkSegments, 0, 0);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function createRoutePredicate(path) {
|
|
130
|
+
return path.includes('**')
|
|
131
|
+
? createWildcardPredicate(path)
|
|
132
|
+
: createBasePredicate(path);
|
|
59
133
|
}
|
|
60
134
|
|
|
135
|
+
// The following functions are adapted from ngx-quicklink,
|
|
136
|
+
// (https://github.com/mgechev/ngx-quicklink)
|
|
137
|
+
// Copyright (c) Minko Gechev and contributors, licensed under the MIT License.
|
|
138
|
+
function isPrimaryRoute(route) {
|
|
139
|
+
return route.outlet === PRIMARY_OUTLET || !route.outlet;
|
|
140
|
+
}
|
|
141
|
+
const findPath = (config, route) => {
|
|
142
|
+
const configQueue = config.slice();
|
|
143
|
+
const parent = new Map();
|
|
144
|
+
const visited = new Set();
|
|
145
|
+
while (configQueue.length) {
|
|
146
|
+
const el = configQueue.shift();
|
|
147
|
+
if (!el) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
visited.add(el);
|
|
151
|
+
if (el === route) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
(el.children || []).forEach((childRoute) => {
|
|
155
|
+
if (!visited.has(childRoute)) {
|
|
156
|
+
parent.set(childRoute, el);
|
|
157
|
+
configQueue.push(childRoute);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const lazyRoutes = el._loadedRoutes || [];
|
|
161
|
+
if (Array.isArray(lazyRoutes)) {
|
|
162
|
+
lazyRoutes.forEach((lazyRoute) => {
|
|
163
|
+
if (lazyRoute && !visited.has(lazyRoute)) {
|
|
164
|
+
parent.set(lazyRoute, el);
|
|
165
|
+
configQueue.push(lazyRoute);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let path = '';
|
|
171
|
+
let currentRoute = route;
|
|
172
|
+
while (currentRoute) {
|
|
173
|
+
const currentPath = currentRoute.path || '';
|
|
174
|
+
if (isPrimaryRoute(currentRoute)) {
|
|
175
|
+
path = `/${currentPath}${path}`;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
path = `/(${currentRoute.outlet}:${currentPath})${path}`;
|
|
179
|
+
}
|
|
180
|
+
currentRoute = parent.get(currentRoute);
|
|
181
|
+
}
|
|
182
|
+
let normalizedPath = path.replaceAll(/\/+/g, '/');
|
|
183
|
+
if (normalizedPath !== '/' && normalizedPath.endsWith('/')) {
|
|
184
|
+
normalizedPath = normalizedPath.slice(0, -1);
|
|
185
|
+
}
|
|
186
|
+
return normalizedPath;
|
|
187
|
+
};
|
|
188
|
+
|
|
61
189
|
/**
|
|
62
190
|
* Type guard to check if a Router Event is a NavigationEnd event.
|
|
63
191
|
* @internal
|
|
@@ -95,8 +223,9 @@ function isNavigationEnd(e) {
|
|
|
95
223
|
* }
|
|
96
224
|
* ```
|
|
97
225
|
*/
|
|
98
|
-
function url() {
|
|
99
|
-
|
|
226
|
+
function url(router) {
|
|
227
|
+
if (!router)
|
|
228
|
+
router = inject(Router);
|
|
100
229
|
return toSignal(router.events.pipe(filter(isNavigationEnd), map((e) => e.urlAfterRedirects)), {
|
|
101
230
|
initialValue: router.url,
|
|
102
231
|
});
|
|
@@ -139,15 +268,15 @@ function leafRoutes() {
|
|
|
139
268
|
const leafRoutes = computed(() => {
|
|
140
269
|
currentUrl();
|
|
141
270
|
return getLeafRoutes(router.routerState.snapshot);
|
|
142
|
-
}, ...(ngDevMode ? [{ debugName: "leafRoutes" }] : []));
|
|
271
|
+
}, ...(ngDevMode ? [{ debugName: "leafRoutes" }] : /* istanbul ignore next */ []));
|
|
143
272
|
return leafRoutes;
|
|
144
273
|
}
|
|
145
274
|
class RouteLeafStore {
|
|
146
275
|
leaves = leafRoutes();
|
|
147
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
148
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
276
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RouteLeafStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
277
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RouteLeafStore, providedIn: 'root' });
|
|
149
278
|
}
|
|
150
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
279
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RouteLeafStore, decorators: [{
|
|
151
280
|
type: Injectable,
|
|
152
281
|
args: [{
|
|
153
282
|
providedIn: 'root',
|
|
@@ -158,6 +287,15 @@ function injectLeafRoutes() {
|
|
|
158
287
|
return store.leaves;
|
|
159
288
|
}
|
|
160
289
|
|
|
290
|
+
function injectSnapshotPathResolver() {
|
|
291
|
+
const router = inject(Router);
|
|
292
|
+
return (route) => {
|
|
293
|
+
const segments = route.pathFromRoot.flatMap((snap) => snap.routeConfig?.path ?? []);
|
|
294
|
+
const joinedSegments = segments.filter(Boolean).join('/');
|
|
295
|
+
return router.serializeUrl(router.parseUrl(joinedSegments));
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
161
299
|
/**
|
|
162
300
|
* @internal
|
|
163
301
|
*/
|
|
@@ -187,6 +325,56 @@ function isInternalBreadcrumb(breadcrumb) {
|
|
|
187
325
|
return !!breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
|
|
188
326
|
}
|
|
189
327
|
|
|
328
|
+
/**
|
|
329
|
+
* @internal
|
|
330
|
+
*/
|
|
331
|
+
const token$2 = new InjectionToken('@mmstack/router-core:breadcrumb-config');
|
|
332
|
+
/**
|
|
333
|
+
* Provides configuration for the breadcrumb system.
|
|
334
|
+
* @param config - A partial `BreadcrumbConfig` object with the desired settings. *
|
|
335
|
+
* @see BreadcrumbConfig
|
|
336
|
+
* @example
|
|
337
|
+
* ```typescript
|
|
338
|
+
* // In your app.module.ts or a standalone component's providers:
|
|
339
|
+
* // import { provideBreadcrumbConfig } from './breadcrumb.config'; // Adjust path
|
|
340
|
+
* // import { ResolvedLeafRoute } from './breadcrumb.type'; // Adjust path
|
|
341
|
+
*
|
|
342
|
+
* // const customLabelStrategy: GenerateBreadcrumbFn = () => {
|
|
343
|
+
* // return (leaf: ResolvedLeafRoute): string => {
|
|
344
|
+
* // // Example: Prioritize a 'navTitle' data property
|
|
345
|
+
* // if (leaf.route.data?.['navTitle']) {
|
|
346
|
+
* // return leaf.route.data['navTitle'];
|
|
347
|
+
* // }
|
|
348
|
+
* // // Fallback to a default mechanism
|
|
349
|
+
* // return leaf.route.title || leaf.segment.resolved || 'Unnamed';
|
|
350
|
+
* // };
|
|
351
|
+
* // };
|
|
352
|
+
*
|
|
353
|
+
* export const appConfig = [
|
|
354
|
+
* // ...rest
|
|
355
|
+
* provideBreadcrumbConfig({
|
|
356
|
+
* generation: customLabelStrategy, // or 'manual' to disable auto-generation
|
|
357
|
+
* }),
|
|
358
|
+
* ]
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
function provideBreadcrumbConfig(config) {
|
|
362
|
+
return {
|
|
363
|
+
provide: token$2,
|
|
364
|
+
useValue: {
|
|
365
|
+
...config,
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* @internal
|
|
371
|
+
*/
|
|
372
|
+
function injectBreadcrumbConfig() {
|
|
373
|
+
return (inject(token$2, {
|
|
374
|
+
optional: true,
|
|
375
|
+
}) ?? {});
|
|
376
|
+
}
|
|
377
|
+
|
|
190
378
|
function uppercaseFirst(str) {
|
|
191
379
|
const lcs = str.toLowerCase();
|
|
192
380
|
return lcs.charAt(0).toUpperCase() + lcs.slice(1);
|
|
@@ -195,7 +383,7 @@ function removeMatrixAndQueryParams(path) {
|
|
|
195
383
|
const [cleanPath] = path.split(';');
|
|
196
384
|
return cleanPath.split('?')[0];
|
|
197
385
|
}
|
|
198
|
-
function parsePathSegment
|
|
386
|
+
function parsePathSegment(pathSegment) {
|
|
199
387
|
return pathSegment
|
|
200
388
|
.split('/')
|
|
201
389
|
.flatMap((part) => part.split('.'))
|
|
@@ -206,319 +394,133 @@ function parsePathSegment$1(pathSegment) {
|
|
|
206
394
|
function generateLabel(leaf) {
|
|
207
395
|
const title = leaf.route.title ?? leaf.route.data?.['title'];
|
|
208
396
|
if (title && typeof title === 'string')
|
|
209
|
-
return title;
|
|
210
|
-
if (leaf.segment.path.includes(':'))
|
|
211
|
-
return leaf.segment.resolved;
|
|
212
|
-
return parsePathSegment
|
|
213
|
-
}
|
|
214
|
-
function autoGenerateBreadcrumb(id, leaf, autoGenerateFn) {
|
|
215
|
-
const label = computed(() => autoGenerateFn()(leaf()), ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
216
|
-
return createInternalBreadcrumb({
|
|
217
|
-
id,
|
|
218
|
-
label,
|
|
219
|
-
ariaLabel: label,
|
|
220
|
-
link: computed(() => leaf().link),
|
|
221
|
-
}, computed(() => leaf().route.data?.['skipBreadcrumb'] !== true &&
|
|
222
|
-
id !== '' &&
|
|
223
|
-
id !== '/' &&
|
|
224
|
-
leaf().segment.path !== '' &&
|
|
225
|
-
leaf().segment.path !== '/' &&
|
|
226
|
-
!leaf().segment.path.endsWith('/') &&
|
|
227
|
-
!!label()));
|
|
228
|
-
}
|
|
229
|
-
function injectGenerateLabelFn() {
|
|
230
|
-
const { generation } = injectBreadcrumbConfig();
|
|
231
|
-
if (typeof generation !== 'function')
|
|
232
|
-
return computed(() => generateLabel);
|
|
233
|
-
const provided = generation();
|
|
234
|
-
return computed(() => provided);
|
|
235
|
-
}
|
|
236
|
-
function injectIsManual() {
|
|
237
|
-
return injectBreadcrumbConfig().generation === 'manual';
|
|
238
|
-
}
|
|
239
|
-
function exposeActiveSignal(crumbSignal, manual) {
|
|
240
|
-
const active = manual
|
|
241
|
-
? computed(() => {
|
|
242
|
-
const crumb = crumbSignal();
|
|
243
|
-
return (isInternalBreadcrumb(crumb) &&
|
|
244
|
-
getBreadcrumbInternals(crumb).registered &&
|
|
245
|
-
getBreadcrumbInternals(crumb).active());
|
|
246
|
-
})
|
|
247
|
-
: computed(() => {
|
|
248
|
-
const crumb = crumbSignal();
|
|
249
|
-
if (!isInternalBreadcrumb(crumb))
|
|
250
|
-
return true;
|
|
251
|
-
return getBreadcrumbInternals(crumb).active();
|
|
252
|
-
});
|
|
253
|
-
const sig = crumbSignal;
|
|
254
|
-
sig.active = active;
|
|
255
|
-
return sig;
|
|
256
|
-
}
|
|
257
|
-
class BreadcrumbStore {
|
|
258
|
-
map = mutable(new Map());
|
|
259
|
-
isManual = injectIsManual();
|
|
260
|
-
autoGenerateLabelFn = injectGenerateLabelFn();
|
|
261
|
-
leafRoutes = injectLeafRoutes();
|
|
262
|
-
all = mapArray(this.leafRoutes, (leaf) => {
|
|
263
|
-
const stableId = computed(() => leaf().path, ...(ngDevMode ? [{ debugName: "stableId" }] : []));
|
|
264
|
-
return exposeActiveSignal(computed(() => {
|
|
265
|
-
const id = stableId();
|
|
266
|
-
const found = this.map().get(id);
|
|
267
|
-
if (!found)
|
|
268
|
-
return autoGenerateBreadcrumb(id, leaf, this.autoGenerateLabelFn);
|
|
269
|
-
if (!id.includes(':'))
|
|
270
|
-
return found;
|
|
271
|
-
return {
|
|
272
|
-
...found,
|
|
273
|
-
link: computed(() => leaf().link),
|
|
274
|
-
};
|
|
275
|
-
}, {
|
|
276
|
-
equal: (a, b) => a.id === b.id,
|
|
277
|
-
}), this.isManual);
|
|
278
|
-
}, {
|
|
279
|
-
equal: (a, b) => a.link === b.link,
|
|
280
|
-
});
|
|
281
|
-
crumbs = computed(() => this.all().filter((c) => c.active()), ...(ngDevMode ? [{ debugName: "crumbs" }] : []));
|
|
282
|
-
unwrapped = computed(() => this.crumbs().map((c) => c()), ...(ngDevMode ? [{ debugName: "unwrapped" }] : []));
|
|
283
|
-
register(breadcrumb) {
|
|
284
|
-
this.map.inline((m) => m.set(breadcrumb.id, breadcrumb));
|
|
285
|
-
}
|
|
286
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BreadcrumbStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
287
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BreadcrumbStore, providedIn: 'root' });
|
|
288
|
-
}
|
|
289
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BreadcrumbStore, decorators: [{
|
|
290
|
-
type: Injectable,
|
|
291
|
-
args: [{
|
|
292
|
-
providedIn: 'root',
|
|
293
|
-
}]
|
|
294
|
-
}] });
|
|
295
|
-
/**
|
|
296
|
-
* Injects and provides access to a reactive list of breadcrumbs.
|
|
297
|
-
*
|
|
298
|
-
* The breadcrumbs are ordered and reflect the current active navigation path.
|
|
299
|
-
* @see Breadcrumb
|
|
300
|
-
* @returns `Signal<Breadcrumb[]>`
|
|
301
|
-
*
|
|
302
|
-
* @example
|
|
303
|
-
* ```typescript
|
|
304
|
-
* @Component({
|
|
305
|
-
* selector: 'app-breadcrumbs',
|
|
306
|
-
* template: `
|
|
307
|
-
* <nav aria-label="breadcrumb">
|
|
308
|
-
* <ol>
|
|
309
|
-
* @for (crumb of breadcrumbs(); track crumb.id) {
|
|
310
|
-
* <li>
|
|
311
|
-
* <a [href]="crumb.link()" [attr.aria-label]="crumb.ariaLabel()">{{ crumb.label() }}</a>
|
|
312
|
-
* </li>
|
|
313
|
-
* }
|
|
314
|
-
* </ol>
|
|
315
|
-
* </nav>
|
|
316
|
-
* `
|
|
317
|
-
* })
|
|
318
|
-
* export class MyBreadcrumbsComponent {
|
|
319
|
-
* breadcrumbs = injectBreadcrumbs();
|
|
320
|
-
* }
|
|
321
|
-
* ```
|
|
322
|
-
*/
|
|
323
|
-
function injectBreadcrumbs() {
|
|
324
|
-
const store = inject(BreadcrumbStore);
|
|
325
|
-
return store.unwrapped;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function parsePathSegment(segmentString) {
|
|
329
|
-
const parts = segmentString.split(';');
|
|
330
|
-
const pathPart = parts[0];
|
|
331
|
-
const matrixParams = {};
|
|
332
|
-
for (let i = 1; i < parts.length; i++) {
|
|
333
|
-
const [key, value = 'true'] = parts[i].split('=');
|
|
334
|
-
if (key) {
|
|
335
|
-
matrixParams[key] = value;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return { pathPart, matrixParams };
|
|
339
|
-
}
|
|
340
|
-
function createBasePredicate(path) {
|
|
341
|
-
const partPredicates = path
|
|
342
|
-
.split('/')
|
|
343
|
-
.filter((part) => !!part.trim())
|
|
344
|
-
.map((configSegmentString) => {
|
|
345
|
-
const { pathPart: configPathPart, matrixParams: configMatrixParams } = parsePathSegment(configSegmentString);
|
|
346
|
-
let singlePathPartPredicate;
|
|
347
|
-
if (configPathPart.startsWith(':')) {
|
|
348
|
-
singlePathPartPredicate = () => true;
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
singlePathPartPredicate = (linkSegmentPathPart) => linkSegmentPathPart === configPathPart;
|
|
352
|
-
}
|
|
353
|
-
const configSegmentHasMatrixParams = Object.keys(configMatrixParams).length > 0;
|
|
354
|
-
return (linkSegmentString) => {
|
|
355
|
-
const { pathPart: linkPathPart, matrixParams: linkMatrixParams } = parsePathSegment(linkSegmentString);
|
|
356
|
-
if (!singlePathPartPredicate(linkPathPart)) {
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
if (!configSegmentHasMatrixParams) {
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
return Object.entries(configMatrixParams).every(([key, value]) => Object.prototype.hasOwnProperty.call(linkMatrixParams, key) &&
|
|
363
|
-
linkMatrixParams[key] === value);
|
|
364
|
-
};
|
|
365
|
-
});
|
|
366
|
-
return (path) => {
|
|
367
|
-
const linkPathOnly = path.split(/[?#]/).at(0) ?? '';
|
|
368
|
-
if (!linkPathOnly && partPredicates.length > 0)
|
|
369
|
-
return false;
|
|
370
|
-
if (!linkPathOnly && partPredicates.length === 0)
|
|
371
|
-
return true;
|
|
372
|
-
const parts = linkPathOnly.split('/').filter((part) => !!part.trim());
|
|
373
|
-
if (parts.length < partPredicates.length)
|
|
374
|
-
return false;
|
|
375
|
-
return parts.every((seg, idx) => {
|
|
376
|
-
const pred = partPredicates.at(idx);
|
|
377
|
-
if (!pred)
|
|
378
|
-
return true;
|
|
379
|
-
return pred(seg);
|
|
380
|
-
});
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
function singleSegmentMatches(configSegment, linkSegment) {
|
|
384
|
-
if (configSegment.pathPart === ':') {
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
else if (configSegment.pathPart !== linkSegment.pathPart) {
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
const configMatrix = configSegment.matrixParams;
|
|
391
|
-
const linkMatrix = linkSegment.matrixParams;
|
|
392
|
-
for (const key in configMatrix) {
|
|
393
|
-
if (!Object.prototype.hasOwnProperty.call(linkMatrix, key) ||
|
|
394
|
-
linkMatrix[key] !== configMatrix[key]) {
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return true;
|
|
399
|
-
}
|
|
400
|
-
function matchSegmentsRecursive(configSegments, linkSegments, configIdx, linkIdx) {
|
|
401
|
-
if (configIdx === configSegments.length) {
|
|
402
|
-
return linkIdx === linkSegments.length;
|
|
403
|
-
}
|
|
404
|
-
if (linkIdx === linkSegments.length) {
|
|
405
|
-
for (let i = configIdx; i < configSegments.length; i++) {
|
|
406
|
-
if (configSegments[i].pathPart !== '**') {
|
|
407
|
-
return false;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
const currentConfigSegment = configSegments[configIdx];
|
|
413
|
-
if (currentConfigSegment.pathPart === '**') {
|
|
414
|
-
if (matchSegmentsRecursive(configSegments, linkSegments, configIdx + 1, linkIdx)) {
|
|
415
|
-
return true;
|
|
416
|
-
}
|
|
417
|
-
if (linkIdx < linkSegments.length) {
|
|
418
|
-
if (matchSegmentsRecursive(configSegments, linkSegments, configIdx, linkIdx + 1)) {
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
return false;
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
if (linkIdx < linkSegments.length &&
|
|
426
|
-
singleSegmentMatches(currentConfigSegment, linkSegments[linkIdx])) {
|
|
427
|
-
return matchSegmentsRecursive(configSegments, linkSegments, configIdx + 1, linkIdx + 1);
|
|
428
|
-
}
|
|
429
|
-
return false;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
function createWildcardPredicate(path) {
|
|
433
|
-
const configSegments = path
|
|
434
|
-
.split('/')
|
|
435
|
-
.filter((p) => !!p.trim())
|
|
436
|
-
.map((segment) => parsePathSegment(segment));
|
|
437
|
-
return (linkPath) => {
|
|
438
|
-
const linkPathOnly = linkPath.split(/[?#]/).at(0) ?? '';
|
|
439
|
-
const linkSegments = linkPathOnly
|
|
440
|
-
.split('/')
|
|
441
|
-
.filter((p) => !!p.trim())
|
|
442
|
-
.map((segment) => parsePathSegment(segment));
|
|
443
|
-
return matchSegmentsRecursive(configSegments, linkSegments, 0, 0);
|
|
444
|
-
};
|
|
397
|
+
return title;
|
|
398
|
+
if (leaf.segment.path.includes(':'))
|
|
399
|
+
return leaf.segment.resolved;
|
|
400
|
+
return parsePathSegment(leaf.segment.path);
|
|
445
401
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
402
|
+
function autoGenerateBreadcrumb(id, leaf, autoGenerateFn) {
|
|
403
|
+
const label = computed(() => autoGenerateFn()(leaf()), ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
404
|
+
return createInternalBreadcrumb({
|
|
405
|
+
id,
|
|
406
|
+
label,
|
|
407
|
+
ariaLabel: label,
|
|
408
|
+
link: computed(() => leaf().link),
|
|
409
|
+
}, computed(() => leaf().route.data?.['skipBreadcrumb'] !== true &&
|
|
410
|
+
id !== '' &&
|
|
411
|
+
id !== '/' &&
|
|
412
|
+
leaf().segment.path !== '' &&
|
|
413
|
+
leaf().segment.path !== '/' &&
|
|
414
|
+
!leaf().segment.path.endsWith('/') &&
|
|
415
|
+
!!label()), false);
|
|
450
416
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return
|
|
417
|
+
function injectGenerateLabelFn() {
|
|
418
|
+
const { generation } = injectBreadcrumbConfig();
|
|
419
|
+
if (typeof generation !== 'function')
|
|
420
|
+
return computed(() => generateLabel);
|
|
421
|
+
const provided = generation();
|
|
422
|
+
return computed(() => provided);
|
|
457
423
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
configQueue.push(childRoute);
|
|
475
|
-
}
|
|
424
|
+
function injectIsManual() {
|
|
425
|
+
return injectBreadcrumbConfig().generation === 'manual';
|
|
426
|
+
}
|
|
427
|
+
function exposeActiveSignal(crumbSignal, manual) {
|
|
428
|
+
const active = manual
|
|
429
|
+
? computed(() => {
|
|
430
|
+
const crumb = crumbSignal();
|
|
431
|
+
return (isInternalBreadcrumb(crumb) &&
|
|
432
|
+
getBreadcrumbInternals(crumb).registered &&
|
|
433
|
+
getBreadcrumbInternals(crumb).active());
|
|
434
|
+
})
|
|
435
|
+
: computed(() => {
|
|
436
|
+
const crumb = crumbSignal();
|
|
437
|
+
if (!isInternalBreadcrumb(crumb))
|
|
438
|
+
return true;
|
|
439
|
+
return getBreadcrumbInternals(crumb).active();
|
|
476
440
|
});
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
441
|
+
const sig = crumbSignal;
|
|
442
|
+
sig.active = active;
|
|
443
|
+
return sig;
|
|
444
|
+
}
|
|
445
|
+
class BreadcrumbStore {
|
|
446
|
+
map = mutable(new Map());
|
|
447
|
+
isManual = injectIsManual();
|
|
448
|
+
autoGenerateLabelFn = injectGenerateLabelFn();
|
|
449
|
+
leafRoutes = injectLeafRoutes();
|
|
450
|
+
all = mapArray(this.leafRoutes, (leaf) => {
|
|
451
|
+
const stableId = computed(() => leaf().path, ...(ngDevMode ? [{ debugName: "stableId" }] : /* istanbul ignore next */ []));
|
|
452
|
+
return exposeActiveSignal(computed(() => {
|
|
453
|
+
const id = stableId();
|
|
454
|
+
const found = this.map().get(id);
|
|
455
|
+
if (!found)
|
|
456
|
+
return autoGenerateBreadcrumb(id, leaf, this.autoGenerateLabelFn);
|
|
457
|
+
if (!id.includes(':'))
|
|
458
|
+
return found;
|
|
459
|
+
return {
|
|
460
|
+
...found,
|
|
461
|
+
link: computed(() => leaf().link),
|
|
462
|
+
};
|
|
463
|
+
}, {
|
|
464
|
+
equal: (a, b) => a.id === b.id,
|
|
465
|
+
}), this.isManual);
|
|
466
|
+
}, {
|
|
467
|
+
equal: (a, b) => a.link === b.link,
|
|
468
|
+
});
|
|
469
|
+
crumbs = computed(() => this.all().filter((c) => c.active()), ...(ngDevMode ? [{ debugName: "crumbs" }] : /* istanbul ignore next */ []));
|
|
470
|
+
unwrapped = computed(() => this.crumbs().map((c) => c()), ...(ngDevMode ? [{ debugName: "unwrapped" }] : /* istanbul ignore next */ []));
|
|
471
|
+
register(breadcrumb) {
|
|
472
|
+
this.map.inline((m) => m.set(breadcrumb.id, breadcrumb));
|
|
502
473
|
}
|
|
503
|
-
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
474
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BreadcrumbStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
475
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BreadcrumbStore, providedIn: 'root' });
|
|
476
|
+
}
|
|
477
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BreadcrumbStore, decorators: [{
|
|
478
|
+
type: Injectable,
|
|
479
|
+
args: [{
|
|
480
|
+
providedIn: 'root',
|
|
481
|
+
}]
|
|
482
|
+
}] });
|
|
483
|
+
/**
|
|
484
|
+
* Injects and provides access to a reactive list of breadcrumbs.
|
|
485
|
+
*
|
|
486
|
+
* The breadcrumbs are ordered and reflect the current active navigation path.
|
|
487
|
+
* @see Breadcrumb
|
|
488
|
+
* @returns `Signal<Breadcrumb[]>`
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```typescript
|
|
492
|
+
* @Component({
|
|
493
|
+
* selector: 'app-breadcrumbs',
|
|
494
|
+
* template: `
|
|
495
|
+
* <nav aria-label="breadcrumb">
|
|
496
|
+
* <ol>
|
|
497
|
+
* @for (crumb of breadcrumbs(); track crumb.id) {
|
|
498
|
+
* <li>
|
|
499
|
+
* <a [href]="crumb.link()" [attr.aria-label]="crumb.ariaLabel()">{{ crumb.label() }}</a>
|
|
500
|
+
* </li>
|
|
501
|
+
* }
|
|
502
|
+
* </ol>
|
|
503
|
+
* </nav>
|
|
504
|
+
* `
|
|
505
|
+
* })
|
|
506
|
+
* export class MyBreadcrumbsComponent {
|
|
507
|
+
* breadcrumbs = injectBreadcrumbs();
|
|
508
|
+
* }
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
511
|
+
function injectBreadcrumbs() {
|
|
512
|
+
const store = inject(BreadcrumbStore);
|
|
513
|
+
return store.unwrapped;
|
|
513
514
|
}
|
|
514
515
|
|
|
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
|
-
*
|
|
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 injectSnapshotPathResolver() {
|
|
|
528
530
|
* path: 'home',
|
|
529
531
|
* component: HomeComponent,
|
|
530
532
|
* resolve: {
|
|
531
|
-
*
|
|
532
|
-
*
|
|
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
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
+
* label: () => userStore.user().name ?? 'Loading...',
|
|
545
|
+
* };
|
|
546
|
+
* }),
|
|
544
547
|
* },
|
|
545
|
-
* }
|
|
548
|
+
* },
|
|
546
549
|
* ];
|
|
547
550
|
* ```
|
|
548
551
|
*/
|
|
549
|
-
function createBreadcrumb(
|
|
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);
|
|
@@ -554,7 +565,7 @@ function createBreadcrumb(factory) {
|
|
|
554
565
|
const fp = resolver(route);
|
|
555
566
|
const tree = createUrlTreeFromSnapshot(route, [], route.queryParams, route.fragment);
|
|
556
567
|
const provided = factory();
|
|
557
|
-
const link = computed(() => router.serializeUrl(tree), ...(ngDevMode ? [{ debugName: "link" }] : []));
|
|
568
|
+
const link = computed(() => router.serializeUrl(tree), ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
|
|
558
569
|
const { label, ariaLabel = label } = provided;
|
|
559
570
|
const bc = {
|
|
560
571
|
id: fp,
|
|
@@ -577,10 +588,10 @@ class PreloadRequester {
|
|
|
577
588
|
startPreload(routePath) {
|
|
578
589
|
this.preloadOnDemand$.next(routePath);
|
|
579
590
|
}
|
|
580
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
581
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
591
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadRequester, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
592
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadRequester, providedIn: 'root' });
|
|
582
593
|
}
|
|
583
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
594
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadRequester, decorators: [{
|
|
584
595
|
type: Injectable,
|
|
585
596
|
args: [{ providedIn: 'root' }]
|
|
586
597
|
}] });
|
|
@@ -620,10 +631,10 @@ class PreloadStrategy {
|
|
|
620
631
|
const predicate = createRoutePredicate(fp);
|
|
621
632
|
return this.req.preloadRequested$.pipe(filter$1((path) => path === fp || predicate(path)), take(1), switchMap(() => load()), finalize(() => this.loading.delete(fp)));
|
|
622
633
|
}
|
|
623
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
624
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
634
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
635
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadStrategy, providedIn: 'root' });
|
|
625
636
|
}
|
|
626
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
637
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadStrategy, decorators: [{
|
|
627
638
|
type: Injectable,
|
|
628
639
|
args: [{
|
|
629
640
|
providedIn: 'root',
|
|
@@ -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);
|
|
@@ -688,26 +731,26 @@ class Link {
|
|
|
688
731
|
}) ?? inject(RouterLinkWithHref, { self: true, optional: true });
|
|
689
732
|
req = inject(PreloadRequester);
|
|
690
733
|
router = inject(Router);
|
|
691
|
-
target = input(...(ngDevMode ? [undefined, { debugName: "target" }] : []));
|
|
692
|
-
queryParams = input(...(ngDevMode ? [undefined, { debugName: "queryParams" }] : []));
|
|
693
|
-
fragment = input(...(ngDevMode ? [undefined, { debugName: "fragment" }] : []));
|
|
694
|
-
queryParamsHandling = input(...(ngDevMode ? [undefined, { debugName: "queryParamsHandling" }] : []));
|
|
695
|
-
state = input(...(ngDevMode ? [undefined, { debugName: "state" }] : []));
|
|
696
|
-
info = input(...(ngDevMode ? [undefined, { debugName: "info" }] : []));
|
|
697
|
-
relativeTo = input(...(ngDevMode ? [undefined, { debugName: "relativeTo" }] : []));
|
|
698
|
-
skipLocationChange = input(false, { ...(ngDevMode ? { debugName: "skipLocationChange" } : {}), transform: booleanAttribute });
|
|
699
|
-
replaceUrl = input(false, { ...(ngDevMode ? { debugName: "replaceUrl" } : {}), transform: booleanAttribute });
|
|
700
|
-
mmLink = input(null, ...(ngDevMode ? [{ debugName: "mmLink" }] : []));
|
|
701
|
-
preloadOn = input(injectConfig().preloadOn, ...(ngDevMode ? [{ debugName: "preloadOn" }] : []));
|
|
702
|
-
useMouseDown = input(injectConfig().useMouseDown, { ...(ngDevMode ? { debugName: "useMouseDown" } : {}), transform: booleanAttribute });
|
|
703
|
-
beforeNavigate = input(...(ngDevMode ? [undefined, { debugName: "beforeNavigate" }] : []));
|
|
734
|
+
target = input(...(ngDevMode ? [undefined, { debugName: "target" }] : /* istanbul ignore next */ []));
|
|
735
|
+
queryParams = input(...(ngDevMode ? [undefined, { debugName: "queryParams" }] : /* istanbul ignore next */ []));
|
|
736
|
+
fragment = input(...(ngDevMode ? [undefined, { debugName: "fragment" }] : /* istanbul ignore next */ []));
|
|
737
|
+
queryParamsHandling = input(...(ngDevMode ? [undefined, { debugName: "queryParamsHandling" }] : /* istanbul ignore next */ []));
|
|
738
|
+
state = input(...(ngDevMode ? [undefined, { debugName: "state" }] : /* istanbul ignore next */ []));
|
|
739
|
+
info = input(...(ngDevMode ? [undefined, { debugName: "info" }] : /* istanbul ignore next */ []));
|
|
740
|
+
relativeTo = input(...(ngDevMode ? [undefined, { debugName: "relativeTo" }] : /* istanbul ignore next */ []));
|
|
741
|
+
skipLocationChange = input(false, { ...(ngDevMode ? { debugName: "skipLocationChange" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
742
|
+
replaceUrl = input(false, { ...(ngDevMode ? { debugName: "replaceUrl" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
743
|
+
mmLink = input(null, ...(ngDevMode ? [{ debugName: "mmLink" }] : /* istanbul ignore next */ []));
|
|
744
|
+
preloadOn = input(injectConfig().preloadOn, ...(ngDevMode ? [{ debugName: "preloadOn" }] : /* istanbul ignore next */ []));
|
|
745
|
+
useMouseDown = input(injectConfig().useMouseDown, { ...(ngDevMode ? { debugName: "useMouseDown" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
746
|
+
beforeNavigate = input(...(ngDevMode ? [undefined, { debugName: "beforeNavigate" }] : /* istanbul ignore next */ []));
|
|
704
747
|
preloading = output();
|
|
705
748
|
urlTree = computed(() => {
|
|
706
749
|
return inputToUrlTree(this.router, this.mmLink(), this.relativeTo(), this.queryParams(), this.fragment(), this.queryParamsHandling(), this.routerLink?.urlTree);
|
|
707
|
-
}, ...(ngDevMode ? [{ debugName: "urlTree" }] : []));
|
|
750
|
+
}, ...(ngDevMode ? [{ debugName: "urlTree" }] : /* istanbul ignore next */ []));
|
|
708
751
|
fullPath = computed(() => {
|
|
709
752
|
return treeToSerializedUrl(this.router, this.urlTree());
|
|
710
|
-
}, ...(ngDevMode ? [{ debugName: "fullPath" }] : []));
|
|
753
|
+
}, ...(ngDevMode ? [{ debugName: "fullPath" }] : /* istanbul ignore next */ []));
|
|
711
754
|
onHover() {
|
|
712
755
|
if (untracked(this.preloadOn) !== 'hover')
|
|
713
756
|
return;
|
|
@@ -743,10 +786,10 @@ class Link {
|
|
|
743
786
|
untracked(this.beforeNavigate)?.();
|
|
744
787
|
return this.routerLink?.onClick(button, ctrlKey, shiftKey, altKey, metaKey);
|
|
745
788
|
}
|
|
746
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
747
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
789
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: Link, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
790
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.12", type: Link, isStandalone: true, selector: "[mmLink]", inputs: { target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, queryParams: { classPropertyName: "queryParams", publicName: "queryParams", isSignal: true, isRequired: false, transformFunction: null }, fragment: { classPropertyName: "fragment", publicName: "fragment", isSignal: true, isRequired: false, transformFunction: null }, queryParamsHandling: { classPropertyName: "queryParamsHandling", publicName: "queryParamsHandling", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, info: { classPropertyName: "info", publicName: "info", isSignal: true, isRequired: false, transformFunction: null }, relativeTo: { classPropertyName: "relativeTo", publicName: "relativeTo", isSignal: true, isRequired: false, transformFunction: null }, skipLocationChange: { classPropertyName: "skipLocationChange", publicName: "skipLocationChange", isSignal: true, isRequired: false, transformFunction: null }, replaceUrl: { classPropertyName: "replaceUrl", publicName: "replaceUrl", isSignal: true, isRequired: false, transformFunction: null }, mmLink: { classPropertyName: "mmLink", publicName: "mmLink", isSignal: true, isRequired: false, transformFunction: null }, preloadOn: { classPropertyName: "preloadOn", publicName: "preloadOn", isSignal: true, isRequired: false, transformFunction: null }, useMouseDown: { classPropertyName: "useMouseDown", publicName: "useMouseDown", isSignal: true, isRequired: false, transformFunction: null }, beforeNavigate: { classPropertyName: "beforeNavigate", publicName: "beforeNavigate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { preloading: "preloading" }, host: { listeners: { "mouseenter": "onHover()", "mousedown": "onMouseDown($event.button,$event.ctrlKey,$event.shiftKey,$event.altKey,$event.metaKey)", "click": "onClick($event.button,$event.ctrlKey,$event.shiftKey,$event.altKey,$event.metaKey)" } }, exportAs: ["mmLink"], hostDirectives: [{ directive: i1.RouterLink, inputs: ["routerLink", "mmLink", "target", "target", "queryParams", "queryParams", "fragment", "fragment", "queryParamsHandling", "queryParamsHandling", "state", "state", "relativeTo", "relativeTo", "skipLocationChange", "skipLocationChange", "replaceUrl", "replaceUrl"] }], ngImport: i0 });
|
|
748
791
|
}
|
|
749
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
792
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: Link, decorators: [{
|
|
750
793
|
type: Directive,
|
|
751
794
|
args: [{
|
|
752
795
|
selector: '[mmLink]',
|
|
@@ -791,6 +834,229 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
|
|
|
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
|
+
* }),
|
|
944
|
+
* ```
|
|
945
|
+
*/
|
|
946
|
+
function provideNavConfig(config) {
|
|
947
|
+
return {
|
|
948
|
+
provide: token$1,
|
|
949
|
+
useValue: { ...config },
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/** @internal */
|
|
953
|
+
function injectNavConfig() {
|
|
954
|
+
return inject(token$1, { optional: true }) ?? {};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** @internal */
|
|
958
|
+
const DEFAULT_NAV_SCOPE = Symbol('mmstack.nav.default');
|
|
959
|
+
class NavStore {
|
|
960
|
+
map = mutable(new Map());
|
|
961
|
+
leafRoutes = injectLeafRoutes();
|
|
962
|
+
/** @internal */
|
|
963
|
+
register(scope, routePath, items) {
|
|
964
|
+
this.map.inline((m) => {
|
|
965
|
+
let scopeMap = m.get(scope);
|
|
966
|
+
if (!scopeMap) {
|
|
967
|
+
scopeMap = new Map();
|
|
968
|
+
m.set(scope, scopeMap);
|
|
969
|
+
}
|
|
970
|
+
scopeMap.set(routePath, items);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
/** @internal */
|
|
974
|
+
scope(name) {
|
|
975
|
+
return computed(() => {
|
|
976
|
+
const scopeMap = this.map().get(name);
|
|
977
|
+
if (!scopeMap)
|
|
978
|
+
return [];
|
|
979
|
+
const leaves = this.leafRoutes();
|
|
980
|
+
for (let i = leaves.length - 1; i >= 0; i--) {
|
|
981
|
+
const items = scopeMap.get(leaves[i].path);
|
|
982
|
+
if (items) {
|
|
983
|
+
return items.filter((it) => !it.hidden());
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return [];
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
990
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, providedIn: 'root' });
|
|
991
|
+
}
|
|
992
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, decorators: [{
|
|
993
|
+
type: Injectable,
|
|
994
|
+
args: [{ providedIn: 'root' }]
|
|
995
|
+
}] });
|
|
996
|
+
/**
|
|
997
|
+
* Returns a reactive list of nav items for the requested scope.
|
|
998
|
+
*
|
|
999
|
+
* The returned signal reflects the nearest active ancestor route that registered items
|
|
1000
|
+
* for `name` via `createNavItems`. Hidden items are filtered out.
|
|
1001
|
+
*
|
|
1002
|
+
* @typeParam TMeta The shape of `NavItem.meta` for the consuming code. Untyped at the
|
|
1003
|
+
* registration site — this is a consumer-side assertion.
|
|
1004
|
+
*
|
|
1005
|
+
* @example
|
|
1006
|
+
* ```typescript
|
|
1007
|
+
* @Component({
|
|
1008
|
+
* template: `
|
|
1009
|
+
* @for (item of items(); track item) {
|
|
1010
|
+
* <a [href]="item.link()" [class.active]="item.active()">{{ item.label() }}</a>
|
|
1011
|
+
* }
|
|
1012
|
+
* `,
|
|
1013
|
+
* })
|
|
1014
|
+
* export class TopBar {
|
|
1015
|
+
* protected readonly items = injectNavItems();
|
|
1016
|
+
* }
|
|
1017
|
+
* ```
|
|
1018
|
+
*/
|
|
1019
|
+
function injectNavItems(name) {
|
|
1020
|
+
const store = inject(NavStore);
|
|
1021
|
+
return store.scope(name ?? DEFAULT_NAV_SCOPE);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Registers a set of nav items for the activating route under the given scope.
|
|
1026
|
+
* Mirrors `createBreadcrumb` / `createTitle` — designed to be used in a route's
|
|
1027
|
+
* `resolve` map.
|
|
1028
|
+
*
|
|
1029
|
+
* Multiple scopes can be registered on a single route by giving each its own `name`
|
|
1030
|
+
* (and a unique key in the `resolve` map):
|
|
1031
|
+
*
|
|
1032
|
+
* ```typescript
|
|
1033
|
+
* resolve: {
|
|
1034
|
+
* mainNav: createNavItems([...], { name: 'main' }),
|
|
1035
|
+
* sideNav: createNavItems([...], { name: 'side' }),
|
|
1036
|
+
* }
|
|
1037
|
+
* ```
|
|
1038
|
+
*
|
|
1039
|
+
* Scope override semantics: when multiple routes in the active chain register items
|
|
1040
|
+
* under the same scope, the deepest active registration wins. Navigating away restores
|
|
1041
|
+
* the shallower registration.
|
|
1042
|
+
*/
|
|
1043
|
+
function createNavItems(itemsOrFactory, options) {
|
|
1044
|
+
const factory = typeof itemsOrFactory === 'function'
|
|
1045
|
+
? itemsOrFactory
|
|
1046
|
+
: () => itemsOrFactory;
|
|
1047
|
+
return async (route) => {
|
|
1048
|
+
const router = inject(Router);
|
|
1049
|
+
const store = inject(NavStore);
|
|
1050
|
+
const resolveRoutePath = injectSnapshotPathResolver();
|
|
1051
|
+
const config = injectNavConfig();
|
|
1052
|
+
const routePath = resolveRoutePath(route);
|
|
1053
|
+
const scope = options?.name ?? DEFAULT_NAV_SCOPE;
|
|
1054
|
+
const items = factory().map((input, i) => createInternalNavItem(input, router, route, config.activeMatch, NEVER_TRUE, NEVER_TRUE, `${routePath}#${i}`));
|
|
1055
|
+
store.register(scope, routePath, items);
|
|
1056
|
+
return Promise.resolve();
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
794
1060
|
/**
|
|
795
1061
|
* Creates a WritableSignal that synchronizes with a specific URL query parameter,
|
|
796
1062
|
* enabling two-way binding between the signal's state and the URL.
|
|
@@ -822,7 +1088,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
|
|
|
822
1088
|
*
|
|
823
1089
|
* @Component({
|
|
824
1090
|
* selector: 'app-product-list',
|
|
825
|
-
* standalone: true,
|
|
826
1091
|
* // imports: [FormsModule], // If using ngModel
|
|
827
1092
|
* template: `
|
|
828
1093
|
* <div>
|
|
@@ -880,7 +1145,7 @@ function queryParam(key, route = inject(ActivatedRoute)) {
|
|
|
880
1145
|
const queryParams = toSignal(route.queryParams, {
|
|
881
1146
|
initialValue: route.snapshot.queryParams,
|
|
882
1147
|
});
|
|
883
|
-
const queryParam = computed(() => queryParamMap().get(keySignal()), ...(ngDevMode ? [{ debugName: "queryParam" }] : []));
|
|
1148
|
+
const queryParam = computed(() => queryParamMap().get(keySignal()), ...(ngDevMode ? [{ debugName: "queryParam" }] : /* istanbul ignore next */ []));
|
|
884
1149
|
const set = (newValue) => {
|
|
885
1150
|
const next = {
|
|
886
1151
|
...untracked(queryParams),
|
|
@@ -901,7 +1166,7 @@ function queryParam(key, route = inject(ActivatedRoute)) {
|
|
|
901
1166
|
return toWritable(queryParam, set);
|
|
902
1167
|
}
|
|
903
1168
|
|
|
904
|
-
const token = new InjectionToken('
|
|
1169
|
+
const token = new InjectionToken('@mmstack/router-core:title-config');
|
|
905
1170
|
/**
|
|
906
1171
|
* used to provide the title configuration, will not be applied unless a `createTitle` resolver is used
|
|
907
1172
|
*/
|
|
@@ -913,29 +1178,41 @@ function provideTitleConfig(config) {
|
|
|
913
1178
|
return {
|
|
914
1179
|
provide: token,
|
|
915
1180
|
useValue: {
|
|
1181
|
+
initialTitle: config?.initialTitle ?? '',
|
|
916
1182
|
parser: prefixFn,
|
|
917
1183
|
keepLastKnown: config?.keepLastKnownTitle ?? true,
|
|
918
1184
|
},
|
|
919
1185
|
};
|
|
920
1186
|
}
|
|
1187
|
+
function identity(str) {
|
|
1188
|
+
return str;
|
|
1189
|
+
}
|
|
921
1190
|
function injectTitleConfig() {
|
|
922
|
-
return inject(token
|
|
1191
|
+
return (inject(token, {
|
|
1192
|
+
optional: true,
|
|
1193
|
+
}) ?? {
|
|
1194
|
+
initialTitle: '',
|
|
1195
|
+
parser: identity,
|
|
1196
|
+
keepLastKnown: true,
|
|
1197
|
+
});
|
|
923
1198
|
}
|
|
924
1199
|
|
|
925
1200
|
class TitleStore {
|
|
926
|
-
title = inject(Title);
|
|
927
1201
|
map = mutable(new Map());
|
|
928
|
-
leafRoutes = injectLeafRoutes();
|
|
929
1202
|
constructor() {
|
|
930
|
-
const
|
|
1203
|
+
const { keepLastKnown, initialTitle } = injectTitleConfig();
|
|
1204
|
+
const leafRoutes = injectLeafRoutes();
|
|
1205
|
+
const title = inject(Title);
|
|
1206
|
+
const fallbackTitle = initialTitle || untracked(() => title.getTitle());
|
|
1207
|
+
const reverseLeaves = computed(() => leafRoutes().toReversed(), ...(ngDevMode ? [{ debugName: "reverseLeaves" }] : /* istanbul ignore next */ []));
|
|
931
1208
|
const currentResolvedTitles = computed(() => {
|
|
932
1209
|
const map = this.map();
|
|
933
1210
|
return reverseLeaves()
|
|
934
|
-
.map((leaf) => map.get(leaf.path)?.() ?? leaf.route.title)
|
|
935
|
-
.filter((v) =>
|
|
936
|
-
}, ...(ngDevMode ? [{ debugName: "currentResolvedTitles" }] : []));
|
|
937
|
-
const currentTitle = computed(() => currentResolvedTitles().at(0) ?? '', ...(ngDevMode ? [{ debugName: "currentTitle" }] : []));
|
|
938
|
-
const heldTitle =
|
|
1211
|
+
.map((leaf) => map.get(leaf.path)?.() ?? leaf.route.title ?? null)
|
|
1212
|
+
.filter((v) => v !== null);
|
|
1213
|
+
}, ...(ngDevMode ? [{ debugName: "currentResolvedTitles" }] : /* istanbul ignore next */ []));
|
|
1214
|
+
const currentTitle = computed(() => currentResolvedTitles().at(0) ?? '', ...(ngDevMode ? [{ debugName: "currentTitle" }] : /* istanbul ignore next */ []));
|
|
1215
|
+
const heldTitle = keepLastKnown
|
|
939
1216
|
? linkedSignal({
|
|
940
1217
|
source: () => currentTitle(),
|
|
941
1218
|
computation: (value, prev) => {
|
|
@@ -946,16 +1223,16 @@ class TitleStore {
|
|
|
946
1223
|
})
|
|
947
1224
|
: currentTitle;
|
|
948
1225
|
effect(() => {
|
|
949
|
-
|
|
1226
|
+
title.setTitle(heldTitle() || fallbackTitle);
|
|
950
1227
|
});
|
|
951
1228
|
}
|
|
952
1229
|
register(id, titleFn) {
|
|
953
1230
|
this.map.inline((m) => m.set(id, titleFn));
|
|
954
1231
|
}
|
|
955
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
956
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
1232
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: TitleStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1233
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: TitleStore, providedIn: 'root' });
|
|
957
1234
|
}
|
|
958
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1235
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: TitleStore, decorators: [{
|
|
959
1236
|
type: Injectable,
|
|
960
1237
|
args: [{
|
|
961
1238
|
providedIn: 'root',
|
|
@@ -965,23 +1242,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
|
|
|
965
1242
|
*
|
|
966
1243
|
* Creates a title resolver function that can be used in Angular's router.
|
|
967
1244
|
*
|
|
968
|
-
* @param
|
|
969
|
-
* A function that returns a string or a Signal<string> representing the title.
|
|
1245
|
+
* @param factoryOrValue
|
|
1246
|
+
* A function that returns a string or a Signal<string> representing the title or just the string directly.
|
|
970
1247
|
* @param awaitValue
|
|
971
1248
|
* If `true`, the resolver will wait until the title signal has a value before resolving.
|
|
972
1249
|
* Defaults to `false`.
|
|
973
1250
|
*/
|
|
974
|
-
function createTitle(
|
|
1251
|
+
function createTitle(factoryOrValue, awaitValue = false) {
|
|
1252
|
+
const factory = typeof factoryOrValue === 'string' ? () => factoryOrValue : factoryOrValue;
|
|
975
1253
|
return async (route) => {
|
|
976
1254
|
const store = inject(TitleStore);
|
|
977
1255
|
const resolver = injectSnapshotPathResolver();
|
|
978
1256
|
const fp = resolver(route);
|
|
979
1257
|
const { parser } = injectTitleConfig();
|
|
980
|
-
const resolved =
|
|
1258
|
+
const resolved = factory();
|
|
981
1259
|
const titleSignal = typeof resolved === 'string'
|
|
982
1260
|
? computed(() => resolved)
|
|
983
1261
|
: computed(resolved);
|
|
984
|
-
const parsedTitleSignal = computed(() => parser(titleSignal()), ...(ngDevMode ? [{ debugName: "parsedTitleSignal" }] : []));
|
|
1262
|
+
const parsedTitleSignal = computed(() => parser(titleSignal()), ...(ngDevMode ? [{ debugName: "parsedTitleSignal" }] : /* istanbul ignore next */ []));
|
|
985
1263
|
store.register(fp, parsedTitleSignal);
|
|
986
1264
|
if (awaitValue)
|
|
987
1265
|
await until(parsedTitleSignal, (v) => !!v);
|
|
@@ -993,5 +1271,5 @@ function createTitle(fn, awaitValue = false) {
|
|
|
993
1271
|
* Generated bundle index. Do not edit.
|
|
994
1272
|
*/
|
|
995
1273
|
|
|
996
|
-
export { Link, PreloadStrategy, createBreadcrumb, createTitle, injectBreadcrumbs, injectTriggerPreload, provideBreadcrumbConfig, provideMMLinkDefaultConfig, provideTitleConfig, queryParam, url };
|
|
1274
|
+
export { Link, PreloadStrategy, createBreadcrumb, createNavItems, createTitle, injectBreadcrumbs, injectNavItems, injectTriggerPreload, provideBreadcrumbConfig, provideMMLinkDefaultConfig, provideNavConfig, provideTitleConfig, queryParam, url };
|
|
997
1275
|
//# sourceMappingURL=mmstack-router-core.mjs.map
|