@cocoar/ui-routing 0.1.0-beta.155
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 +349 -0
- package/fesm2022/cocoar-ui-routing.mjs +236 -0
- package/fesm2022/cocoar-ui-routing.mjs.map +1 -0
- package/package.json +49 -0
- package/types/cocoar-ui-routing.d.ts +194 -0
package/README.md
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# @cocoar/ui-routing
|
|
2
|
+
|
|
3
|
+
Fragment-based routing utilities for Angular applications. Enable deep-linkable components (modals, drawers, panels) and custom actions through URL fragments.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Fragment-as-Route Pattern** — Treat URL fragments (`#details/123`) like declarative routes
|
|
8
|
+
- **Path Parameter Extraction** — Use path patterns (`:id`, `:customer`) in fragments
|
|
9
|
+
- **Query Parameter Support** — Parse and type query parameters (`?edit=true&mode=advanced`)
|
|
10
|
+
- **Component Deep-linking** — Open any component via URL (modals, drawers, panels, etc.)
|
|
11
|
+
- **Action Handlers** — Execute side effects through fragment changes
|
|
12
|
+
- **Framework-Agnostic** — No assumptions about UI implementation (bring your own overlay system)
|
|
13
|
+
- **Type-Safe** — Full TypeScript support with inference
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @cocoar/ui-routing
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Define Fragment Routes
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { ComponentRoutedFragment, ActionRoutedFragment } from '@cocoar/ui-routing';
|
|
27
|
+
|
|
28
|
+
// For modals, drawers, or any component-based UI:
|
|
29
|
+
const fragments: ComponentRoutedFragment[] = [
|
|
30
|
+
{
|
|
31
|
+
type: 'component',
|
|
32
|
+
path: 'details/:id',
|
|
33
|
+
loadComponent: () => import('./details.component').then(m => m.DetailsComponent),
|
|
34
|
+
options: { width: '800px', closeOnBackdrop: true } // Your custom config
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// For actions without UI:
|
|
39
|
+
const actionFragments: ActionRoutedFragment[] = [
|
|
40
|
+
{
|
|
41
|
+
type: 'action',
|
|
42
|
+
path: 'logout',
|
|
43
|
+
handler: () => authService.logout()
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Add to Route Configuration
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { createRouteData, IRoutedFragmentConfig } from '@cocoar/ui-routing';
|
|
52
|
+
|
|
53
|
+
const routes: Routes = [
|
|
54
|
+
{
|
|
55
|
+
path: 'dashboard',
|
|
56
|
+
component: DashboardComponent,
|
|
57
|
+
data: createRouteData<IRoutedFragmentConfig>({
|
|
58
|
+
routedFragments: fragments
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. React to Fragment Changes
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { RoutedFragmentService } from '@cocoar/ui-routing';
|
|
68
|
+
|
|
69
|
+
@Component({ /* ... */ })
|
|
70
|
+
export class DashboardComponent {
|
|
71
|
+
private fragmentService = inject(RoutedFragmentService);
|
|
72
|
+
private modalService = inject(MyModalService); // Your modal implementation
|
|
73
|
+
|
|
74
|
+
ngOnInit() {
|
|
75
|
+
// Handle component fragments (modals, drawers, etc.)
|
|
76
|
+
this.fragmentService.getParsedFragments('component').subscribe(components => {
|
|
77
|
+
components.forEach(async item => {
|
|
78
|
+
const component = await item.route.loadComponent();
|
|
79
|
+
this.modalService.open(component, {
|
|
80
|
+
data: item.params,
|
|
81
|
+
...item.route.options // Your custom options
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Handle action fragments
|
|
87
|
+
this.fragmentService.getParsedFragments('action').subscribe(actions => {
|
|
88
|
+
actions.forEach(item => item.route.handler(item.params));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```component via fragment
|
|
93
|
+
this.router.navigate([], { fragment: 'details/123?edit=true' });
|
|
94
|
+
|
|
95
|
+
// Multiple fragments (component + confirmation)
|
|
96
|
+
this.router.navigate([], { fragment: 'details/123#confirm' });
|
|
97
|
+
|
|
98
|
+
// Remove fragment (close component)
|
|
99
|
+
this.fragmentService.removeFragmentPart('details/123');
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## API Reference
|
|
103
|
+
|
|
104
|
+
### Types
|
|
105
|
+
|
|
106
|
+
#### `ComponentRoutedFragment<TOptions>`
|
|
107
|
+
Configuration for component fragments (modals, drawers, panels, etc.) with generic options.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
interface ComponentRoutedFragment<TOptions = unknown> {
|
|
111
|
+
type: 'component';
|
|
112
|
+
path: string; // Path pattern (e.g., 'details/:id/:tab')
|
|
113
|
+
loadComponent: () => Type<unknown> | Promise<Type<unknown>>;
|
|
114
|
+
options?: TOptions; // Your custom
|
|
115
|
+
interface ModalRoutedFragment<TModalOptions = unknown> {
|
|
116
|
+
type: 'modal';
|
|
117
|
+
path: string; // Path pattern (e.g., 'details/:id/:tab')
|
|
118
|
+
loadComponent: () => Type<unknown> | Promise<Type<unknown>>;
|
|
119
|
+
modalOptions?: TModalOptions; // Your overlay config type
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### `ActionRoutedFragment`
|
|
124
|
+
Configuration for action fragments (no UI).
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
interface ActionRoutedFragment extends RoutedFragmentBase<never> {
|
|
128
|
+
type: 'action';
|
|
129
|
+
handler: (params: any) => void;
|
|
130
|
+
options?: never; // Actions don't have options
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### `IRoutedFragmentConfig<TFragment>`
|
|
135
|
+
Route data configuration. Generic parameter allows type-safe fragment arrays.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface IRoutedFragmentConfig<TFragment extends RoutedFragmentBase = RoutedFragmentBase> {
|
|
139
|
+
routedFragments: TFragment[];
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### `ParsedRoute<T>`
|
|
144
|
+
Parsed fragment with extracted parameters.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
interface ParsedRoute<T extends RoutedFragment = RoutedFragment> {
|
|
148
|
+
params: { [key: string]: any }; // Extracted path & query params
|
|
149
|
+
route: T; // Matched route configuration
|
|
150
|
+
fragment: string; // Original fragment string
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Services
|
|
155
|
+
|
|
156
|
+
#### `RoutedFragmentService`
|
|
157
|
+
component fragments (modals, drawers, etc.)
|
|
158
|
+
fragmentService.getParsedFragments('component').subscribe(components => {
|
|
159
|
+
// Handle components
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Get action fragments
|
|
163
|
+
fragmentService.getParsedFragments('action').subscribe(actions => {
|
|
164
|
+
// Handle actionmits parsed fragments filtered bycomponents).
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
fragmentService.removeFragmentPart('details/123');
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Integration Examples
|
|
171
|
+
|
|
172
|
+
The library is completely generic and works with any UI system. Here are integration patterns:
|
|
173
|
+
|
|
174
|
+
### With Modals (Angular Material Dialog)
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { MatDialogConfig } from '@angular/material/dialog';
|
|
178
|
+
import { ComponentRoutedFragment } from '@cocoar/ui-routing';
|
|
179
|
+
|
|
180
|
+
type ModalFragment = ComponentRoutedFragment<MatDialogConfig>;
|
|
181
|
+
|
|
182
|
+
const fragments: ModalFragment[] = [{
|
|
183
|
+
type: 'component',
|
|
184
|
+
path: 'details/:id',
|
|
185
|
+
loadComponent: () => DetailsComponent,
|
|
186
|
+
options: { width: '600px', hasBackdrop: true }
|
|
187
|
+
}];
|
|
188
|
+
|
|
189
|
+
// In component:
|
|
190
|
+
this.fragmentService.getParsedFragments('component').subscribe(items => {
|
|
191
|
+
items.forEach(async item => {
|
|
192
|
+
const component = await item.route.loadComponent();
|
|
193
|
+
this.dialog.open(component, {
|
|
194
|
+
data: item.params,
|
|
195
|
+
...item.route.options
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### With Drawers/Side Panels
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
interface DrawerConfig {
|
|
205
|
+
position: 'left' | 'right';
|
|
206
|
+
width: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
type DrawerFragment = ComponentRoutedFragment<DrawerConfig>;
|
|
210
|
+
|
|
211
|
+
const fragments: DrawerFragment[] = [{
|
|
212
|
+
type: 'component',
|
|
213
|
+
path: 'settings',
|
|
214
|
+
loadComponent: () => SettingsComponent,
|
|
215
|
+
options: { position: 'right', width: '400px' }
|
|
216
|
+
}];
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### With Custom Overlay System
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
interface MyOverlayConfig {
|
|
223
|
+
width?: string;
|
|
224
|
+
closeOnEscape?: boolean;
|
|
225
|
+
backdrop?: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
type CustomFragment = ComponentRoutedFragment<MyOverlayConfig>;
|
|
229
|
+
|
|
230
|
+
// Create a service that bridges fragments → your overlay system
|
|
231
|
+
@Injectable({ providedIn: 'root' })
|
|
232
|
+
export class RoutedOverlayService {
|
|
233
|
+
private fragmentService = inject(RoutedFragmentService);
|
|
234
|
+
private overlayService = inject(MyOverlayService);
|
|
235
|
+
|
|
236
|
+
constructor() {
|
|
237
|
+
this.fragmentService.getParsedFragments('component').subscribe(items => {
|
|
238
|
+
items.forEach(async item => {
|
|
239
|
+
const component = await item.route.loadComponent();
|
|
240
|
+
this.overlayService.open(component, {
|
|
241
|
+
data: item.params,
|
|
242
|
+
config: item.route.options
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}loseOnEscape?: boolean;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
type CustomModalFragment = ModalRoutedFragment<MyOverlayConfig>;
|
|
251
|
+
|
|
252
|
+
const fragments: CustomModalFragment[] = [{
|
|
253
|
+
type: 'modal',
|
|
254
|
+
path: 'settings',
|
|
255
|
+
loadComponent: () => SettingsComponent,
|
|
256
|
+
modalOptions: { width: '400px', closeOnEscape: true }
|
|
257
|
+
}];
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Advanced Usage
|
|
261
|
+
|
|
262
|
+
### Extending with Custom Fragment Types
|
|
263
|
+
|
|
264
|
+
The library uses a **base interface pattern** that allows you to create your own fragment types:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { RoutedFragmentBase } from '@cocoar/ui-routing';
|
|
268
|
+
import { Type } from '@angular/core';
|
|
269
|
+
|
|
270
|
+
// 1. Define your custom fragment type
|
|
271
|
+
export interface DrawerRoutedFragment extends RoutedFragmentBase<DrawerConfig> {
|
|
272
|
+
type: 'drawer';
|
|
273
|
+
side: 'left' | 'right';
|
|
274
|
+
loadComponent: () => Type<unknown> | Promise<Type<unknown>>;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface DrawerConfig {
|
|
278
|
+
width: string;
|
|
279
|
+
backdrop?: boolean;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 2. Create fragments
|
|
283
|
+
const drawerFragments: DrawerRoutedFragment[] = [{
|
|
284
|
+
type: 'drawer',
|
|
285
|
+
path: 'filters',
|
|
286
|
+
side: 'left',
|
|
287
|
+
loadComponent: () => import('./filters-drawer.component'),
|
|
288
|
+
options: { width: '300px', backdrop: true }
|
|
289
|
+
}];
|
|
290
|
+
|
|
291
|
+
// 3. React to your custom type
|
|
292
|
+
this.fragmentService.getParsedFragments('drawer').subscribe(drawers => {
|
|
293
|
+
drawers.forEach(async item => {
|
|
294
|
+
const component = await item.route.loadComponent();
|
|
295
|
+
// item.route is typed as DrawerRoutedFragment
|
|
296
|
+
this.drawerService.open(component, item.route.side, item.route.options);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Multiple Fragments
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// URL: /page#details/123#confirm
|
|
305
|
+
// Opens both "details" component AND "confirm" component
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Query Parameters
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// Fragment: details/123?edit=true&tab=settings
|
|
312
|
+
// Parsed params: { id: '123', edit: true, tab: 'settings' }
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Dynamic Parameters
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
{
|
|
319
|
+
type: 'component',
|
|
320
|
+
path: 'user/:userId/order/:orderId',
|
|
321
|
+
loadComponent: () => OrderDetailsComponent
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Fragment: user/42/order/1337
|
|
325
|
+
// Params: { userId: '42', orderId: '1337' }
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Accessibility Considerations
|
|
329
|
+
|
|
330
|
+
⚠️ **Important:** Fragment changes do not automatically announce to screen readers. When opening components via fragments:
|
|
331
|
+
|
|
332
|
+
1. Ensure proper ARIA attributes (`role`, `aria-labelledby`, `aria-describedby`)
|
|
333
|
+
2. Trap focus within overlays
|
|
334
|
+
3. Return focus to trigger element on close
|
|
335
|
+
4. Consider announcing state changes with `aria-live` regions
|
|
336
|
+
|
|
337
|
+
## Browser Support
|
|
338
|
+
|
|
339
|
+
- Modern browsers (Chrome, Firefox, Safari, Edge)
|
|
340
|
+
- Requires Angular 21+ and Angular Router
|
|
341
|
+
|
|
342
|
+
## License
|
|
343
|
+
|
|
344
|
+
Apache-2.0
|
|
345
|
+
|
|
346
|
+
## Contributing
|
|
347
|
+
|
|
348
|
+
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines.
|
|
349
|
+
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { match } from 'path-to-regexp';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { InjectionToken, inject, Injectable } from '@angular/core';
|
|
4
|
+
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
|
|
5
|
+
import { map, filter } from 'rxjs/operators';
|
|
6
|
+
import { BehaviorSubject, merge, defer } from 'rxjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Type-safe helper for creating route data configuration.
|
|
10
|
+
* Ensures TypeScript inference for route data objects.
|
|
11
|
+
*
|
|
12
|
+
* @param data - Route data object
|
|
13
|
+
* @returns The same object with proper typing
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const routeData = createRouteData({
|
|
18
|
+
* routedFragments: [...]
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function createRouteData(data) {
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parses URL fragment into structured routes with parameters.
|
|
28
|
+
* Supports multiple fragments separated by '#' and query parameters.
|
|
29
|
+
*
|
|
30
|
+
* @param fragment - Raw URL fragment (e.g., "details/123?edit=true#confirm")
|
|
31
|
+
* @param registeredRoutes - Array of route configurations to match against
|
|
32
|
+
* @returns Array of parsed routes with extracted parameters
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const routes = [
|
|
37
|
+
* { type: 'component', path: 'details/:id', loadComponent: () => DetailsComponent }
|
|
38
|
+
* ];
|
|
39
|
+
* const parsed = parseFragment('details/123?edit=true', routes);
|
|
40
|
+
* // Result: [{ params: { id: '123', edit: true }, route: {...}, fragment: 'details/123?edit=true' }]
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
function parseFragment(fragment, registeredRoutes) {
|
|
44
|
+
// Normalize routes: expand arrays into individual route entries
|
|
45
|
+
const normalizedRoutes = registeredRoutes.flatMap((route) => {
|
|
46
|
+
if (Array.isArray(route.path)) {
|
|
47
|
+
// Expand array paths into separate route objects
|
|
48
|
+
return route.path.map((p) => ({ ...route, path: p }));
|
|
49
|
+
}
|
|
50
|
+
return [route];
|
|
51
|
+
});
|
|
52
|
+
const routeGroups = fragment.split('#'); // Split on '#'
|
|
53
|
+
return routeGroups
|
|
54
|
+
.map((routeGroup) => {
|
|
55
|
+
const [routePath, queryParamsString] = routeGroup.split('?'); // Separate query params
|
|
56
|
+
const parsedParams = {};
|
|
57
|
+
// Find the registered route that matches the full routePath
|
|
58
|
+
const registeredRoute = normalizedRoutes.find((route) => {
|
|
59
|
+
const matcher = match(route.path, { decode: decodeURIComponent });
|
|
60
|
+
const matchResult = matcher(routePath);
|
|
61
|
+
return !!matchResult; // Return true if the route matches the full routePath
|
|
62
|
+
});
|
|
63
|
+
if (registeredRoute) {
|
|
64
|
+
const matcher = match(registeredRoute.path, { decode: decodeURIComponent });
|
|
65
|
+
const matchResult = matcher(routePath);
|
|
66
|
+
if (matchResult) {
|
|
67
|
+
Object.assign(parsedParams, matchResult.params); // Extract route params (like id, customer)
|
|
68
|
+
}
|
|
69
|
+
// Handle optional query parameters
|
|
70
|
+
if (queryParamsString) {
|
|
71
|
+
const queryParams = new URLSearchParams(queryParamsString);
|
|
72
|
+
queryParams.forEach((value, key) => {
|
|
73
|
+
let parsedValue;
|
|
74
|
+
// Try to parse as JSON (for booleans, numbers, etc.)
|
|
75
|
+
try {
|
|
76
|
+
parsedValue = JSON.parse(value);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
parsedValue = value; // Default to string if JSON parsing fails
|
|
80
|
+
}
|
|
81
|
+
parsedParams[key] = parsedValue;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Return the matched route and its parameters
|
|
85
|
+
return { fragment: routeGroup, params: parsedParams, route: registeredRoute };
|
|
86
|
+
}
|
|
87
|
+
// Return null if no match is found (handled in filtering)
|
|
88
|
+
return null;
|
|
89
|
+
})
|
|
90
|
+
.filter((route) => route !== null); // Filter out null results and return empty array if nothing matches
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Injection token for providing routed fragments configuration.
|
|
95
|
+
* Use this when you can't provide fragments via route data (e.g., in scenarios or tests).
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* providers: [
|
|
100
|
+
* { provide: ROUTED_FRAGMENTS, useValue: [...fragments] }
|
|
101
|
+
* ]
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
const ROUTED_FRAGMENTS = new InjectionToken('ROUTED_FRAGMENTS');
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Service that monitors Angular Router fragments and parses them into structured routes.
|
|
108
|
+
* Provides observables for reacting to fragment changes.
|
|
109
|
+
*
|
|
110
|
+
* Fragment configuration can be provided via:
|
|
111
|
+
* 1. ROUTED_FRAGMENTS injection token (preferred for scenarios/tests)
|
|
112
|
+
* 2. Route data with `routedFragments` property
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* // Via injection token:
|
|
117
|
+
* providers: [
|
|
118
|
+
* RoutedFragmentService,
|
|
119
|
+
* { provide: ROUTED_FRAGMENTS, useValue: fragments }
|
|
120
|
+
* ]
|
|
121
|
+
*
|
|
122
|
+
* // Via route data:
|
|
123
|
+
* {
|
|
124
|
+
* path: 'page',
|
|
125
|
+
* component: MyComponent,
|
|
126
|
+
* data: { routedFragments: fragments }
|
|
127
|
+
* }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
class RoutedFragmentService {
|
|
131
|
+
router = inject(Router);
|
|
132
|
+
activatedRoute = inject(ActivatedRoute);
|
|
133
|
+
injectedFragments = inject(ROUTED_FRAGMENTS, { optional: true });
|
|
134
|
+
parsedFragments = new BehaviorSubject([]);
|
|
135
|
+
/**
|
|
136
|
+
* Get parsed fragments filtered by type.
|
|
137
|
+
* Emits whenever the fragment changes via router navigation.
|
|
138
|
+
*
|
|
139
|
+
* @param type - Fragment type to filter by ('component' | 'action' | your custom type)
|
|
140
|
+
* @returns Observable of parsed fragments matching the specified type
|
|
141
|
+
*/
|
|
142
|
+
getParsedFragments(type) {
|
|
143
|
+
return this.parsedFragments.pipe(map((pf) => pf.filter((p) => p.route.type === type)));
|
|
144
|
+
}
|
|
145
|
+
getRoutedFragments = (routeSnapshot) => {
|
|
146
|
+
// Prefer injected fragments (for scenarios/tests), fall back to route data
|
|
147
|
+
if (this.injectedFragments && this.injectedFragments.length > 0) {
|
|
148
|
+
return this.injectedFragments;
|
|
149
|
+
}
|
|
150
|
+
return routeSnapshot.data?.routedFragments ?? [];
|
|
151
|
+
};
|
|
152
|
+
getCurrentRoute = (route) => {
|
|
153
|
+
while (route.firstChild) {
|
|
154
|
+
route = route.firstChild;
|
|
155
|
+
}
|
|
156
|
+
return route;
|
|
157
|
+
};
|
|
158
|
+
constructor() {
|
|
159
|
+
// Combine initial fragment state with navigation events
|
|
160
|
+
// Using merge ensures both initial load and subsequent navigations are handled
|
|
161
|
+
merge(
|
|
162
|
+
// Emit current state immediately
|
|
163
|
+
defer(() => {
|
|
164
|
+
const snapshot = this.getCurrentRoute(this.activatedRoute).snapshot;
|
|
165
|
+
return [{ fragment: snapshot.fragment, fragments: this.getRoutedFragments(snapshot) }];
|
|
166
|
+
}),
|
|
167
|
+
// Watch for navigation events
|
|
168
|
+
this.router.events.pipe(filter((event) => event instanceof NavigationEnd), map(() => {
|
|
169
|
+
const snapshot = this.getCurrentRoute(this.activatedRoute).snapshot;
|
|
170
|
+
return { fragment: snapshot.fragment, fragments: this.getRoutedFragments(snapshot) };
|
|
171
|
+
}))).subscribe(({ fragment, fragments }) => {
|
|
172
|
+
if (fragments && fragments.length > 0) {
|
|
173
|
+
this.handleFragment(fragment ?? '', fragments);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async handleFragment(fragment, routedFragments) {
|
|
178
|
+
const parsedRoutes = parseFragment(fragment, routedFragments);
|
|
179
|
+
this.parsedFragments.next(parsedRoutes);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Remove a specific fragment part from the current URL.
|
|
183
|
+
* Useful for closing modals or clearing fragment state.
|
|
184
|
+
*
|
|
185
|
+
* @param part - Fragment path prefix to remove (e.g., 'details/123')
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* // URL: /page#details/123#confirm
|
|
190
|
+
* fragmentService.removeFragmentPart('details/123');
|
|
191
|
+
* // Result: /page#confirm
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
removeFragmentPart(part) {
|
|
195
|
+
const fragment = this.router.url.split('#')[1] || ''; // Get current fragment
|
|
196
|
+
const fragmentParts = fragment.split('#').map(fullyDecodeURIComponent); // Split multiple fragment parts
|
|
197
|
+
// Filter out the fragment part that matches the provided path
|
|
198
|
+
const updatedFragment = fragmentParts.filter((frag) => !frag.startsWith(part)).join('#');
|
|
199
|
+
const route = this.getCurrentRoute(this.activatedRoute);
|
|
200
|
+
// Use the router to navigate with the updated fragment
|
|
201
|
+
this.router.navigate([], {
|
|
202
|
+
relativeTo: route,
|
|
203
|
+
queryParams: route.snapshot.queryParams, // Preserve the current query parameters
|
|
204
|
+
fragment: updatedFragment || undefined, // If no fragment remains, set to undefined
|
|
205
|
+
replaceUrl: false, // Do not replace the current URL in the history
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RoutedFragmentService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
209
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RoutedFragmentService, providedIn: 'root' });
|
|
210
|
+
}
|
|
211
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RoutedFragmentService, decorators: [{
|
|
212
|
+
type: Injectable,
|
|
213
|
+
args: [{ providedIn: 'root' }]
|
|
214
|
+
}], ctorParameters: () => [] });
|
|
215
|
+
function fullyDecodeURIComponent(input) {
|
|
216
|
+
let prev = '';
|
|
217
|
+
let current = input;
|
|
218
|
+
try {
|
|
219
|
+
do {
|
|
220
|
+
prev = current;
|
|
221
|
+
current = decodeURIComponent(current);
|
|
222
|
+
} while (current !== prev);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// If decodeURIComponent fails (e.g., due to an incomplete %xx), abort
|
|
226
|
+
return prev;
|
|
227
|
+
}
|
|
228
|
+
return current;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Generated bundle index. Do not edit.
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
export { ROUTED_FRAGMENTS, RoutedFragmentService, createRouteData, parseFragment };
|
|
236
|
+
//# sourceMappingURL=cocoar-ui-routing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cocoar-ui-routing.mjs","sources":["../../../../libs/ui-routing/src/lib/routed-fragments/create-route-data.ts","../../../../libs/ui-routing/src/lib/routed-fragments/fragment-parser.ts","../../../../libs/ui-routing/src/lib/routed-fragments/routed-fragment.ts","../../../../libs/ui-routing/src/lib/routed-fragments/routed-fragment.service.ts","../../../../libs/ui-routing/src/cocoar-ui-routing.ts"],"sourcesContent":["/**\n * Type-safe helper for creating route data configuration.\n * Ensures TypeScript inference for route data objects.\n *\n * @param data - Route data object\n * @returns The same object with proper typing\n *\n * @example\n * ```typescript\n * const routeData = createRouteData({\n * routedFragments: [...]\n * });\n * ```\n */\nexport function createRouteData<T>(data: T): T {\n return data;\n}\n","import { match } from 'path-to-regexp';\nimport { RoutedFragmentBase } from './routed-fragment';\n\n/**\n * Parsed fragment with extracted parameters and matched route.\n */\nexport interface ParsedRoute<T extends RoutedFragmentBase = RoutedFragmentBase> {\n params: Record<string, unknown>;\n route: T;\n fragment: string;\n}\n\n/**\n * Parses URL fragment into structured routes with parameters.\n * Supports multiple fragments separated by '#' and query parameters.\n *\n * @param fragment - Raw URL fragment (e.g., \"details/123?edit=true#confirm\")\n * @param registeredRoutes - Array of route configurations to match against\n * @returns Array of parsed routes with extracted parameters\n *\n * @example\n * ```typescript\n * const routes = [\n * { type: 'component', path: 'details/:id', loadComponent: () => DetailsComponent }\n * ];\n * const parsed = parseFragment('details/123?edit=true', routes);\n * // Result: [{ params: { id: '123', edit: true }, route: {...}, fragment: 'details/123?edit=true' }]\n * ```\n */\nexport function parseFragment<T extends RoutedFragmentBase>(\n fragment: string,\n registeredRoutes: T[]\n): ParsedRoute<T>[] {\n // Normalize routes: expand arrays into individual route entries\n const normalizedRoutes = registeredRoutes.flatMap((route) => {\n if (Array.isArray(route.path)) {\n // Expand array paths into separate route objects\n return route.path.map((p) => ({ ...route, path: p }));\n }\n return [route];\n });\n\n const routeGroups = fragment.split('#'); // Split on '#'\n\n return routeGroups\n .map((routeGroup) => {\n const [routePath, queryParamsString] = routeGroup.split('?'); // Separate query params\n const parsedParams: Record<string, unknown> = {};\n\n // Find the registered route that matches the full routePath\n const registeredRoute = normalizedRoutes.find((route) => {\n const matcher = match(route.path, { decode: decodeURIComponent });\n const matchResult = matcher(routePath);\n return !!matchResult; // Return true if the route matches the full routePath\n });\n\n if (registeredRoute) {\n const matcher = match(registeredRoute.path, { decode: decodeURIComponent });\n const matchResult = matcher(routePath);\n\n if (matchResult) {\n Object.assign(parsedParams, matchResult.params); // Extract route params (like id, customer)\n }\n\n // Handle optional query parameters\n if (queryParamsString) {\n const queryParams = new URLSearchParams(queryParamsString);\n queryParams.forEach((value, key) => {\n let parsedValue: unknown;\n\n // Try to parse as JSON (for booleans, numbers, etc.)\n try {\n parsedValue = JSON.parse(value);\n } catch {\n parsedValue = value; // Default to string if JSON parsing fails\n }\n\n parsedParams[key] = parsedValue;\n });\n }\n\n // Return the matched route and its parameters\n return { fragment: routeGroup, params: parsedParams, route: registeredRoute };\n }\n\n // Return null if no match is found (handled in filtering)\n return null;\n })\n .filter((route) => route !== null); // Filter out null results and return empty array if nothing matches\n}\n","import { InjectionToken, Type } from '@angular/core';\n\n/**\n * Base interface for all routed fragments.\n * Extend this interface to create custom fragment types in your application.\n *\n * @example\n * ```typescript\n * // In your app:\n * export interface DrawerRoutedFragment extends RoutedFragmentBase<DrawerConfig> {\n * type: 'drawer';\n * loadComponent: () => Type<unknown> | Promise<Type<unknown>>;\n * }\n * ```\n */\nexport interface RoutedFragmentBase<TOptions = unknown> {\n type: string;\n path: string | string[];\n options?: TOptions;\n}\n\n/**\n * Route configuration interface for fragment-based routing.\n * Add this to Angular route data to enable fragment parsing.\n */\nexport interface IRoutedFragmentConfig<TFragment extends RoutedFragmentBase = RoutedFragmentBase> {\n routedFragments: TFragment[];\n}\n\n/**\n * Injection token for providing routed fragments configuration.\n * Use this when you can't provide fragments via route data (e.g., in scenarios or tests).\n *\n * @example\n * ```typescript\n * providers: [\n * { provide: ROUTED_FRAGMENTS, useValue: [...fragments] }\n * ]\n * ```\n */\nexport const ROUTED_FRAGMENTS = new InjectionToken<RoutedFragmentBase[]>('ROUTED_FRAGMENTS');\n\n/**\n * Component fragment that loads a lazy-loaded component.\n * Generic TOptions allows consumers to provide their own configuration type\n * (e.g., modal options, drawer options, dialog options, etc.).\n *\n * @example\n * ```typescript\n * // For modals:\n * const fragment: ComponentRoutedFragment<MyModalConfig> = {\n * type: 'component',\n * path: 'details/:id',\n * loadComponent: () => import('./details.component'),\n * options: { width: '800px', closeOnBackdrop: true }\n * };\n *\n * // For drawers:\n * const fragment: ComponentRoutedFragment<MyDrawerConfig> = {\n * type: 'component',\n * path: 'settings',\n * loadComponent: () => import('./settings.component'),\n * options: { position: 'right', width: '400px' }\n * };\n * ```\n */\nexport interface ComponentRoutedFragment<TOptions = unknown> extends RoutedFragmentBase<TOptions> {\n type: 'component';\n loadComponent: () => Type<unknown> | Promise<Type<unknown>>;\n}\n\n/**\n * Action fragment that executes a custom handler.\n * Useful for triggering side effects without UI components.\n *\n * @example\n * ```typescript\n * const fragment: ActionRoutedFragment = {\n * type: 'action',\n * path: 'logout',\n * handler: (params) => authService.logout()\n * };\n * ```\n */\nexport interface ActionRoutedFragment extends RoutedFragmentBase<never> {\n type: 'action';\n handler: (params: Record<string, unknown>) => void;\n options?: never; // Actions don't have options\n}\n","import { inject, Injectable } from '@angular/core';\nimport { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';\nimport { filter, map } from 'rxjs/operators';\nimport { BehaviorSubject, defer, merge } from 'rxjs';\n\nimport { ParsedRoute, parseFragment } from './fragment-parser';\nimport { IRoutedFragmentConfig, RoutedFragmentBase, ROUTED_FRAGMENTS } from './routed-fragment';\n\n/**\n * Service that monitors Angular Router fragments and parses them into structured routes.\n * Provides observables for reacting to fragment changes.\n *\n * Fragment configuration can be provided via:\n * 1. ROUTED_FRAGMENTS injection token (preferred for scenarios/tests)\n * 2. Route data with `routedFragments` property\n *\n * @example\n * ```typescript\n * // Via injection token:\n * providers: [\n * RoutedFragmentService,\n * { provide: ROUTED_FRAGMENTS, useValue: fragments }\n * ]\n *\n * // Via route data:\n * {\n * path: 'page',\n * component: MyComponent,\n * data: { routedFragments: fragments }\n * }\n * ```\n */\n@Injectable({ providedIn: 'root' })\nexport class RoutedFragmentService {\n private router = inject(Router);\n private activatedRoute = inject(ActivatedRoute);\n private injectedFragments = inject(ROUTED_FRAGMENTS, { optional: true });\n\n private parsedFragments = new BehaviorSubject<ParsedRoute[]>([]);\n\n /**\n * Get parsed fragments filtered by type.\n * Emits whenever the fragment changes via router navigation.\n *\n * @param type - Fragment type to filter by ('component' | 'action' | your custom type)\n * @returns Observable of parsed fragments matching the specified type\n */\n public getParsedFragments<T extends string>(type: T) {\n return this.parsedFragments.pipe(\n map((pf) =>\n pf.filter((p): p is ParsedRoute<RoutedFragmentBase & { type: T }> => p.route.type === type)\n )\n );\n }\n\n private getRoutedFragments = (routeSnapshot: ActivatedRouteSnapshot) => {\n // Prefer injected fragments (for scenarios/tests), fall back to route data\n if (this.injectedFragments && this.injectedFragments.length > 0) {\n return this.injectedFragments;\n }\n\n return (routeSnapshot.data as IRoutedFragmentConfig)?.routedFragments ?? [];\n };\n\n private getCurrentRoute = (route: ActivatedRoute): ActivatedRoute => {\n while (route.firstChild) {\n route = route.firstChild;\n }\n return route;\n };\n\n constructor() {\n // Combine initial fragment state with navigation events\n // Using merge ensures both initial load and subsequent navigations are handled\n merge(\n // Emit current state immediately\n defer(() => {\n const snapshot = this.getCurrentRoute(this.activatedRoute).snapshot;\n return [{ fragment: snapshot.fragment, fragments: this.getRoutedFragments(snapshot) }];\n }),\n // Watch for navigation events\n this.router.events.pipe(\n filter((event): event is NavigationEnd => event instanceof NavigationEnd),\n map(() => {\n const snapshot = this.getCurrentRoute(this.activatedRoute).snapshot;\n return { fragment: snapshot.fragment, fragments: this.getRoutedFragments(snapshot) };\n })\n )\n ).subscribe(({ fragment, fragments }) => {\n if (fragments && fragments.length > 0) {\n this.handleFragment(fragment ?? '', fragments);\n }\n });\n }\n\n private async handleFragment(fragment: string, routedFragments: RoutedFragmentBase[]) {\n const parsedRoutes = parseFragment(fragment, routedFragments);\n this.parsedFragments.next(parsedRoutes);\n }\n\n /**\n * Remove a specific fragment part from the current URL.\n * Useful for closing modals or clearing fragment state.\n *\n * @param part - Fragment path prefix to remove (e.g., 'details/123')\n *\n * @example\n * ```typescript\n * // URL: /page#details/123#confirm\n * fragmentService.removeFragmentPart('details/123');\n * // Result: /page#confirm\n * ```\n */\n public removeFragmentPart(part: string) {\n const fragment = this.router.url.split('#')[1] || ''; // Get current fragment\n const fragmentParts = fragment.split('#').map(fullyDecodeURIComponent); // Split multiple fragment parts\n\n // Filter out the fragment part that matches the provided path\n const updatedFragment = fragmentParts.filter((frag) => !frag.startsWith(part)).join('#');\n\n const route = this.getCurrentRoute(this.activatedRoute);\n\n // Use the router to navigate with the updated fragment\n this.router.navigate([], {\n relativeTo: route,\n queryParams: route.snapshot.queryParams, // Preserve the current query parameters\n fragment: updatedFragment || undefined, // If no fragment remains, set to undefined\n replaceUrl: false, // Do not replace the current URL in the history\n });\n }\n}\n\nfunction fullyDecodeURIComponent(input: string): string {\n let prev = '';\n let current = input;\n\n try {\n do {\n prev = current;\n current = decodeURIComponent(current);\n } while (current !== prev);\n } catch {\n // If decodeURIComponent fails (e.g., due to an incomplete %xx), abort\n return prev;\n }\n\n return current;\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cocoar/ui-routing",
|
|
3
|
+
"version": "0.1.0-beta.155",
|
|
4
|
+
"description": "Fragment-based routing utilities for Angular applications",
|
|
5
|
+
"author": "Cocoar",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/cocoar-dev/cocoar-ui.git",
|
|
10
|
+
"directory": "libs/ui-routing"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/cocoar-dev/cocoar-ui/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/cocoar-dev/cocoar-ui",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"angular",
|
|
18
|
+
"routing",
|
|
19
|
+
"fragments",
|
|
20
|
+
"url-fragments",
|
|
21
|
+
"modal-routing",
|
|
22
|
+
"deep-linking",
|
|
23
|
+
"cocoar"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@angular/core": "^21.0.0",
|
|
30
|
+
"@angular/router": "^21.0.0",
|
|
31
|
+
"rxjs": "^7.8.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"path-to-regexp": "^8.3.0",
|
|
35
|
+
"tslib": "^2.3.0"
|
|
36
|
+
},
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"module": "fesm2022/cocoar-ui-routing.mjs",
|
|
39
|
+
"typings": "types/cocoar-ui-routing.d.ts",
|
|
40
|
+
"exports": {
|
|
41
|
+
"./package.json": {
|
|
42
|
+
"default": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./types/cocoar-ui-routing.d.ts",
|
|
46
|
+
"default": "./fesm2022/cocoar-ui-routing.mjs"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, Type } from '@angular/core';
|
|
3
|
+
import * as rxjs from 'rxjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type-safe helper for creating route data configuration.
|
|
7
|
+
* Ensures TypeScript inference for route data objects.
|
|
8
|
+
*
|
|
9
|
+
* @param data - Route data object
|
|
10
|
+
* @returns The same object with proper typing
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const routeData = createRouteData({
|
|
15
|
+
* routedFragments: [...]
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare function createRouteData<T>(data: T): T;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Base interface for all routed fragments.
|
|
23
|
+
* Extend this interface to create custom fragment types in your application.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // In your app:
|
|
28
|
+
* export interface DrawerRoutedFragment extends RoutedFragmentBase<DrawerConfig> {
|
|
29
|
+
* type: 'drawer';
|
|
30
|
+
* loadComponent: () => Type<unknown> | Promise<Type<unknown>>;
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
interface RoutedFragmentBase<TOptions = unknown> {
|
|
35
|
+
type: string;
|
|
36
|
+
path: string | string[];
|
|
37
|
+
options?: TOptions;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Route configuration interface for fragment-based routing.
|
|
41
|
+
* Add this to Angular route data to enable fragment parsing.
|
|
42
|
+
*/
|
|
43
|
+
interface IRoutedFragmentConfig<TFragment extends RoutedFragmentBase = RoutedFragmentBase> {
|
|
44
|
+
routedFragments: TFragment[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Injection token for providing routed fragments configuration.
|
|
48
|
+
* Use this when you can't provide fragments via route data (e.g., in scenarios or tests).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* providers: [
|
|
53
|
+
* { provide: ROUTED_FRAGMENTS, useValue: [...fragments] }
|
|
54
|
+
* ]
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare const ROUTED_FRAGMENTS: InjectionToken<RoutedFragmentBase<unknown>[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Component fragment that loads a lazy-loaded component.
|
|
60
|
+
* Generic TOptions allows consumers to provide their own configuration type
|
|
61
|
+
* (e.g., modal options, drawer options, dialog options, etc.).
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* // For modals:
|
|
66
|
+
* const fragment: ComponentRoutedFragment<MyModalConfig> = {
|
|
67
|
+
* type: 'component',
|
|
68
|
+
* path: 'details/:id',
|
|
69
|
+
* loadComponent: () => import('./details.component'),
|
|
70
|
+
* options: { width: '800px', closeOnBackdrop: true }
|
|
71
|
+
* };
|
|
72
|
+
*
|
|
73
|
+
* // For drawers:
|
|
74
|
+
* const fragment: ComponentRoutedFragment<MyDrawerConfig> = {
|
|
75
|
+
* type: 'component',
|
|
76
|
+
* path: 'settings',
|
|
77
|
+
* loadComponent: () => import('./settings.component'),
|
|
78
|
+
* options: { position: 'right', width: '400px' }
|
|
79
|
+
* };
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
interface ComponentRoutedFragment<TOptions = unknown> extends RoutedFragmentBase<TOptions> {
|
|
83
|
+
type: 'component';
|
|
84
|
+
loadComponent: () => Type<unknown> | Promise<Type<unknown>>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Action fragment that executes a custom handler.
|
|
88
|
+
* Useful for triggering side effects without UI components.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const fragment: ActionRoutedFragment = {
|
|
93
|
+
* type: 'action',
|
|
94
|
+
* path: 'logout',
|
|
95
|
+
* handler: (params) => authService.logout()
|
|
96
|
+
* };
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
interface ActionRoutedFragment extends RoutedFragmentBase<never> {
|
|
100
|
+
type: 'action';
|
|
101
|
+
handler: (params: Record<string, unknown>) => void;
|
|
102
|
+
options?: never;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parsed fragment with extracted parameters and matched route.
|
|
107
|
+
*/
|
|
108
|
+
interface ParsedRoute<T extends RoutedFragmentBase = RoutedFragmentBase> {
|
|
109
|
+
params: Record<string, unknown>;
|
|
110
|
+
route: T;
|
|
111
|
+
fragment: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parses URL fragment into structured routes with parameters.
|
|
115
|
+
* Supports multiple fragments separated by '#' and query parameters.
|
|
116
|
+
*
|
|
117
|
+
* @param fragment - Raw URL fragment (e.g., "details/123?edit=true#confirm")
|
|
118
|
+
* @param registeredRoutes - Array of route configurations to match against
|
|
119
|
+
* @returns Array of parsed routes with extracted parameters
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const routes = [
|
|
124
|
+
* { type: 'component', path: 'details/:id', loadComponent: () => DetailsComponent }
|
|
125
|
+
* ];
|
|
126
|
+
* const parsed = parseFragment('details/123?edit=true', routes);
|
|
127
|
+
* // Result: [{ params: { id: '123', edit: true }, route: {...}, fragment: 'details/123?edit=true' }]
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
declare function parseFragment<T extends RoutedFragmentBase>(fragment: string, registeredRoutes: T[]): ParsedRoute<T>[];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Service that monitors Angular Router fragments and parses them into structured routes.
|
|
134
|
+
* Provides observables for reacting to fragment changes.
|
|
135
|
+
*
|
|
136
|
+
* Fragment configuration can be provided via:
|
|
137
|
+
* 1. ROUTED_FRAGMENTS injection token (preferred for scenarios/tests)
|
|
138
|
+
* 2. Route data with `routedFragments` property
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* // Via injection token:
|
|
143
|
+
* providers: [
|
|
144
|
+
* RoutedFragmentService,
|
|
145
|
+
* { provide: ROUTED_FRAGMENTS, useValue: fragments }
|
|
146
|
+
* ]
|
|
147
|
+
*
|
|
148
|
+
* // Via route data:
|
|
149
|
+
* {
|
|
150
|
+
* path: 'page',
|
|
151
|
+
* component: MyComponent,
|
|
152
|
+
* data: { routedFragments: fragments }
|
|
153
|
+
* }
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare class RoutedFragmentService {
|
|
157
|
+
private router;
|
|
158
|
+
private activatedRoute;
|
|
159
|
+
private injectedFragments;
|
|
160
|
+
private parsedFragments;
|
|
161
|
+
/**
|
|
162
|
+
* Get parsed fragments filtered by type.
|
|
163
|
+
* Emits whenever the fragment changes via router navigation.
|
|
164
|
+
*
|
|
165
|
+
* @param type - Fragment type to filter by ('component' | 'action' | your custom type)
|
|
166
|
+
* @returns Observable of parsed fragments matching the specified type
|
|
167
|
+
*/
|
|
168
|
+
getParsedFragments<T extends string>(type: T): rxjs.Observable<ParsedRoute<RoutedFragmentBase<unknown> & {
|
|
169
|
+
type: T;
|
|
170
|
+
}>[]>;
|
|
171
|
+
private getRoutedFragments;
|
|
172
|
+
private getCurrentRoute;
|
|
173
|
+
constructor();
|
|
174
|
+
private handleFragment;
|
|
175
|
+
/**
|
|
176
|
+
* Remove a specific fragment part from the current URL.
|
|
177
|
+
* Useful for closing modals or clearing fragment state.
|
|
178
|
+
*
|
|
179
|
+
* @param part - Fragment path prefix to remove (e.g., 'details/123')
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* // URL: /page#details/123#confirm
|
|
184
|
+
* fragmentService.removeFragmentPart('details/123');
|
|
185
|
+
* // Result: /page#confirm
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
removeFragmentPart(part: string): void;
|
|
189
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RoutedFragmentService, never>;
|
|
190
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<RoutedFragmentService>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { ROUTED_FRAGMENTS, RoutedFragmentService, createRouteData, parseFragment };
|
|
194
|
+
export type { ActionRoutedFragment, ComponentRoutedFragment, IRoutedFragmentConfig, ParsedRoute, RoutedFragmentBase };
|