@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 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 };