Fragment-based routing utilities for Angular applications
npm install @cocoar/ui-routingFragment-based routing utilities for Angular applications. Enable deep-linkable components (modals, drawers, panels) and custom actions through URL fragments.
- Fragment-as-Route Pattern — Treat URL fragments (#details/123) like declarative routes
- Path Parameter Extraction — Use path patterns (:id, :customer) in fragments
- Query Parameter Support — Parse and type query parameters (?edit=true&mode=advanced)
- Component Deep-linking — Open any component via URL (modals, drawers, panels, etc.)
- Action Handlers — Execute side effects through fragment changes
- Framework-Agnostic — No assumptions about UI implementation (bring your own overlay system)
- Type-Safe — Full TypeScript support with inference
``bash`
npm install @cocoar/ui-routing
`typescript
import { ComponentRoutedFragment, ActionRoutedFragment } from '@cocoar/ui-routing';
// For modals, drawers, or any component-based UI:
const fragments: ComponentRoutedFragment[] = [
{
type: 'component',
path: 'details/:id',
loadComponent: () => import('./details.component').then(m => m.DetailsComponent),
options: { width: '800px', closeOnBackdrop: true } // Your custom config
}
];
// For actions without UI:
const actionFragments: ActionRoutedFragment[] = [
{
type: 'action',
path: 'logout',
handler: () => authService.logout()
}
];
`
`typescript
import { createRouteData, IRoutedFragmentConfig } from '@cocoar/ui-routing';
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
data: createRouteData
routedFragments: fragments
})
}
];
`
`typescript
import { RoutedFragmentService } from '@cocoar/ui-routing';
@Component({ / ... / })
export class DashboardComponent {
private fragmentService = inject(RoutedFragmentService);
private modalService = inject(MyModalService); // Your modal implementation
ngOnInit() {
// Handle component fragments (modals, drawers, etc.)
this.fragmentService.getParsedFragments('component').subscribe(components => {
components.forEach(async item => {
const component = await item.route.loadComponent();
this.modalService.open(component, {
data: item.params,
...item.route.options // Your custom options
});
});
});
// Handle action fragments
this.fragmentService.getParsedFragments('action').subscribe(actions => {
actions.forEach(item => item.route.handler(item.params));
});
}
}
`component via fragment
this.router.navigate([], { fragment: 'details/123?edit=true' });
// Multiple fragments (component + confirmation)
this.router.navigate([], { fragment: 'details/123#confirm' });
// Remove fragment (close component)
this.fragmentService.removeFragmentPart('details/123');
`
#### ComponentRoutedFragment
Configuration for component fragments (modals, drawers, panels, etc.) with generic options.
`typescript`
interface ComponentRoutedFragment
type: 'component';
path: string; // Path pattern (e.g., 'details/:id/:tab')
loadComponent: () => Type
options?: TOptions; // Your custom
interface ModalRoutedFragment
type: 'modal';
path: string; // Path pattern (e.g., 'details/:id/:tab')
loadComponent: () => Type
modalOptions?: TModalOptions; // Your overlay config type
}
#### ActionRoutedFragment
Configuration for action fragments (no UI).
`typescript`
interface ActionRoutedFragment extends RoutedFragmentBase
type: 'action';
handler: (params: any) => void;
options?: never; // Actions don't have options
}
#### IRoutedFragmentConfig
Route data configuration. Generic parameter allows type-safe fragment arrays.
`typescript`
interface IRoutedFragmentConfig
routedFragments: TFragment[];
}
#### ParsedRoute
Parsed fragment with extracted parameters.
`typescript`
interface ParsedRoute
params: { [key: string]: any }; // Extracted path & query params
route: T; // Matched route configuration
fragment: string; // Original fragment string
}
#### RoutedFragmentService
component fragments (modals, drawers, etc.)
fragmentService.getParsedFragments('component').subscribe(components => {
// Handle components
});
// Get action fragments
fragmentService.getParsedFragments('action').subscribe(actions => {
// Handle actionmits parsed fragments filtered bycomponents).
`typescript`
fragmentService.removeFragmentPart('details/123');
The library is completely generic and works with any UI system. Here are integration patterns:
`typescript
import { MatDialogConfig } from '@angular/material/dialog';
import { ComponentRoutedFragment } from '@cocoar/ui-routing';
type ModalFragment = ComponentRoutedFragment
const fragments: ModalFragment[] = [{
type: 'component',
path: 'details/:id',
loadComponent: () => DetailsComponent,
options: { width: '600px', hasBackdrop: true }
}];
// In component:
this.fragmentService.getParsedFragments('component').subscribe(items => {
items.forEach(async item => {
const component = await item.route.loadComponent();
this.dialog.open(component, {
data: item.params,
...item.route.options
});
});
});
`
`typescript
interface DrawerConfig {
position: 'left' | 'right';
width: string;
}
type DrawerFragment = ComponentRoutedFragment
const fragments: DrawerFragment[] = [{
type: 'component',
path: 'settings',
loadComponent: () => SettingsComponent,
options: { position: 'right', width: '400px' }
}];
`
`typescript
interface MyOverlayConfig {
width?: string;
closeOnEscape?: boolean;
backdrop?: boolean;
}
type CustomFragment = ComponentRoutedFragment
// Create a service that bridges fragments → your overlay system
@Injectable({ providedIn: 'root' })
export class RoutedOverlayService {
private fragmentService = inject(RoutedFragmentService);
private overlayService = inject(MyOverlayService);
constructor() {
this.fragmentService.getParsedFragments('component').subscribe(items => {
items.forEach(async item => {
const component = await item.route.loadComponent();
this.overlayService.open(component, {
data: item.params,
config: item.route.options
});
});
});
}
}loseOnEscape?: boolean;
}
type CustomModalFragment = ModalRoutedFragment
const fragments: CustomModalFragment[] = [{
type: 'modal',
path: 'settings',
loadComponent: () => SettingsComponent,
modalOptions: { width: '400px', closeOnEscape: true }
}];
`
The library uses a base interface pattern that allows you to create your own fragment types:
`typescript
import { RoutedFragmentBase } from '@cocoar/ui-routing';
import { Type } from '@angular/core';
// 1. Define your custom fragment type
export interface DrawerRoutedFragment extends RoutedFragmentBase
type: 'drawer';
side: 'left' | 'right';
loadComponent: () => Type
}
export interface DrawerConfig {
width: string;
backdrop?: boolean;
}
// 2. Create fragments
const drawerFragments: DrawerRoutedFragment[] = [{
type: 'drawer',
path: 'filters',
side: 'left',
loadComponent: () => import('./filters-drawer.component'),
options: { width: '300px', backdrop: true }
}];
// 3. React to your custom type
this.fragmentService.getParsedFragments('drawer').subscribe(drawers => {
drawers.forEach(async item => {
const component = await item.route.loadComponent();
// item.route is typed as DrawerRoutedFragment
this.drawerService.open(component, item.route.side, item.route.options);
});
});
`
`typescript`
// URL: /page#details/123#confirm
// Opens both "details" component AND "confirm" component
`typescript`
// Fragment: details/123?edit=true&tab=settings
// Parsed params: { id: '123', edit: true, tab: 'settings' }
`typescript
{
type: 'component',
path: 'user/:userId/order/:orderId',
loadComponent: () => OrderDetailsComponent
}
// Fragment: user/42/order/1337
// Params: { userId: '42', orderId: '1337' }
`
⚠️ Important: Fragment changes do not automatically announce to screen readers. When opening components via fragments:
1. Ensure proper ARIA attributes (role, aria-labelledby, aria-describedby)aria-live` regions
2. Trap focus within overlays
3. Return focus to trigger element on close
4. Consider announcing state changes with
- Modern browsers (Chrome, Firefox, Safari, Edge)
- Requires Angular 21+ and Angular Router
Apache-2.0
See CONTRIBUTING.md for guidelines.