Fragment Parser & Modal Routing
Parse URL fragments (hash portion) into structured routes with parameters. Combined with composables, this enables deep-linkable modals — open modals via URL, share links, and use browser back to close them.
Separate Package
pnpm add @cocoar/vue-fragment-parserimport {
parseFragment, // Core parser
useFragmentNavigation, // navigateToModal(), closeModal()
useRoutedFragments, // Reactive fragment parsing
useRoutedModals, // Auto dialog/modal from URL
type DialogFragment, // type: 'dialog' route config
type ModalFragment, // type: 'modal' route config
} from '@cocoar/vue-fragment-parser';Basic Usage
Define routes with path patterns and parse the current URL fragment:
import { parseFragment, type RoutedFragmentBase } from '@cocoar/vue-fragment-parser';
interface AppRoute extends RoutedFragmentBase {
type: string;
path: string | string[];
options?: { requiresAuth?: boolean };
}
const routes: AppRoute[] = [
{ type: 'overview', path: 'overview' },
{ type: 'details', path: 'details/:id' },
{ type: 'edit', path: 'details/:id/edit' },
];
// URL: https://app.com/#details/42?tab=comments
const result = parseFragment('#details/42?tab=comments', routes);
// → [{ route: { type: 'details', ... }, params: { id: '42', tab: 'comments' }, fragment: '...' }]Path Parameters
Use :param syntax for dynamic segments (powered by path-to-regexp):
const routes = [
{ type: 'user', path: 'user/:userId' },
{ type: 'project', path: 'project/:projectId/task/:taskId' },
];
// #user/abc → params: { userId: 'abc' }
// #project/1/task/42 → params: { projectId: '1', taskId: '42' }Query Parameters
Query parameters are parsed automatically with JSON type coercion:
// #details/5?edit=true&count=3
// → params: { id: '5', edit: true, count: 3 }
// #overview?tags=["a","b"]
// → params: { tags: ['a', 'b'] }Multiple Fragments
Chain multiple fragments with # for composable routing:
// #details/5#confirm?force=true
const results = parseFragment('#details/5#confirm?force=true', routes);
// → Two parsed routes: details + confirmArray Paths
A single route can match multiple paths:
const routes = [
{ type: 'docs', path: ['overview', 'usage', 'examples'] },
];
// #overview → matches docs route
// #usage → matches docs route
// #examples → matches docs routeModal Routing
The fragment parser's main use case: deep-linkable modals. When a user double-clicks a grid row, the modal opens AND the URL updates. Copy-pasting that URL opens the same modal. Browser back closes it.
/todos → just the list
/todos#todo-42 → list + detail modal for todo-42
/todos#todo-42?tab=2 → list + detail modal, comments tab selectedHow It Works
Three composables work together:
User clicks row
↓
navigateToModal('todo-42') ← useFragmentNavigation
↓
URL changes to /todos#todo-42
↓
useRoutedFragments detects change ← useRoutedFragments
↓
Parses fragment against routes
↓
useRoutedModals opens dialog ← useRoutedModals
↓
User closes dialog (or browser back)
↓
Fragment removed from URLStep 1: Define Fragment Routes
Register which fragments should open components in your Vue Router config. Two types are supported:
type: 'dialog'— Opens inside aCoarDialogshell (header, title, close button)type: 'modal'— Opens as a raw overlay (no shell, your component IS the entire modal)
// routes.ts
import type { RoutedOverlayFragment } from '@cocoar/vue-fragment-parser';
const routes = [
{
path: '/todos',
component: () => import('./TodoList.vue'),
meta: {
routedFragments: [
{
type: 'dialog',
path: ':todoId',
component: () => import('./TodoDetail.vue'),
dialogOptions: { title: 'Todo Details', size: 'l' },
},
] satisfies RoutedOverlayFragment[],
},
},
];Step 2: Wire Up the List View
<!-- TodoList.vue -->
<script setup lang="ts">
import { useFragmentNavigation, useRoutedModals } from '@cocoar/vue-fragment-parser';
// Auto-open/close modals based on URL fragments
useRoutedModals();
// Navigate to modal on interaction
const { navigateToModal } = useFragmentNavigation();
builder.onRowDoubleClicked((event) => {
navigateToModal(event.data.id);
});
// With query params:
// navigateToModal(event.data.id, { tab: 0 })
// → URL: #todo-42?tab=0
</script>Step 3: Build the Modal Content
The modal component receives fragment params as props, plus a close function:
<!-- TodoDetail.vue -->
<script setup lang="ts">
const props = defineProps<{
todoId: string; // from fragment path ':todoId'
tab?: number; // from query params '?tab=2'
close: (result?: unknown) => void;
}>();
</script>
<template>
<div>
<h3>Todo: {{ todoId }}</h3>
<button @click="close()">Done</button>
</div>
</template>Deep-Linking
Copy the URL https://app.com/todos#todo-42?tab=2 and paste it in a new browser tab. The page loads, the fragment is parsed, and the modal opens automatically with todoId: 'todo-42' and tab: 2.
Browser Back & History
The modal system integrates with browser history:
- Modal open → creates a history entry (
/todos→/todos#todo-42) - Modal close (X button) → creates another entry (
/todos#todo-42→/todos) - Browser Back after close → goes back to
/todos#todo-42→ modal reopens - Browser Back while modal is open → goes back to
/todos→ modal closes
This means users can navigate modal state with the browser's back/forward buttons, just like regular pages.
Multiple Modals
By default, navigateToModal replaces the current fragment. Use append: true to open multiple modals at once:
// Opens modal, replaces any existing fragment
navigateToModal('todo-42');
// URL: /todos#todo-42
// Opens second modal alongside the first
navigateToModal('confirm', undefined, { append: true });
// URL: /todos#todo-42#confirmEach fragment is matched independently. Closing one modal removes only its fragment.
Composable API
useFragmentNavigation()
const { navigateToModal, closeModal } = useFragmentNavigation();| Method | Parameters | Description |
|---|---|---|
navigateToModal(path, params?, options?) | string, Record<string, ...>?, { append?: boolean }? | Set fragment in URL, opens modal. append: true for multi-modal. |
closeModal(path) | string | Remove fragment from URL, closes modal |
useRoutedFragments(routes?)
const { fragments } = useRoutedFragments();Reactively parses route.hash against routes from route.meta.routedFragments. Returns computed<ParsedRoute[]>.
useRoutedModals()
useRoutedModals();Fire-and-forget composable. Watches fragments and manages the overlay lifecycle:
type: 'dialog'→ opens viauseDialog().open()withDialogConfig(shell with header/title)type: 'modal'→ opens viagetOverlayService().open()withOverlaySpec(raw overlay, no shell)
Handles:
- Fragment appears → lazy-load component → open dialog/modal
- Fragment removed (close button, browser back) → close
- Closed by user → remove fragment from URL
- Page load with fragment → deep-link: opens immediately
API
parseFragment<T>(fragment, routes)
| Parameter | Type | Description |
|---|---|---|
fragment | string | URL fragment string (with or without leading #) |
routes | T[] | Array of route definitions |
Returns: ParsedRoute<T>[] — array of matched routes
Types
interface RoutedFragmentBase<TOptions = unknown> {
type: string;
path: string | string[];
options?: TOptions;
}
interface ParsedRoute<T extends RoutedFragmentBase> {
params: Record<string, unknown>; // Path + query parameters
route: T; // Matched route config
fragment: string; // Original fragment string
}