Complete reference for the Eleva Router plugin.
| Property | Type | Description |
|---|---|---|
eleva |
Eleva |
Parent Eleva instance |
options |
RouterOptions |
Merged configuration options |
routes |
RouteDefinition[] |
Registered routes array |
emitter |
Emitter |
Event emitter instance |
isStarted |
boolean |
Whether router is started |
currentRoute |
Signal<RouteLocation \| null> |
Current route (reactive) |
previousRoute |
Signal<RouteLocation \| null> |
Previous route (reactive) |
currentParams |
Signal<Record<string, string>> |
Route params (reactive) |
currentQuery |
Signal<Record<string, string>> |
Query params (reactive) |
currentLayout |
Signal<MountResult \| null> |
Layout instance (reactive) |
currentView |
Signal<MountResult \| null> |
View instance (reactive) |
isReady |
Signal<boolean> |
Ready state (reactive) |
| Method | Signature | Description |
|---|---|---|
start |
() => Promise<Router> |
Start the router |
stop |
() => Promise<void> |
Stop the router |
destroy |
() => Promise<void> |
Stop and cleanup (alias: stop) |
navigate |
(location, params?) => Promise<boolean> |
Navigate to route |
onBeforeEach |
(guard) => () => void |
Register global guard |
onAfterEnter |
(hook) => () => void |
Register after-enter hook |
onAfterLeave |
(hook) => () => void |
Register after-leave hook |
onAfterEach |
(hook) => () => void |
Register after-each hook |
onError |
(handler) => () => void |
Register error handler |
addRoute |
(route) => () => void |
Add route dynamically |
removeRoute |
(path) => boolean |
Remove route by path |
hasRoute |
(path) => boolean |
Check if route exists |
getRoutes |
() => RouteDefinition[] |
Get all routes |
getRoute |
(path) => RouteDefinition \| undefined |
Get route by path |
use |
(plugin, options?) => void |
Install plugin |
getPlugins |
() => RouterPlugin[] |
Get all plugins |
getPlugin |
(name) => RouterPlugin \| undefined |
Get plugin by name |
removePlugin |
(name) => boolean |
Remove plugin |
setErrorHandler |
(handler) => void |
Set error handler |
The primary method for programmatic navigation.
Signatures:
// Navigate by path string
await router.navigate("/users/123");
// Navigate by path with query
await router.navigate("/search?q=hello&page=1");
// Navigate with route object
await router.navigate({
path: "/users/:id",
params: { id: "123" },
query: { tab: "settings" },
hash: "#section"
});
// Navigate with replace (no history entry)
await router.navigate("/dashboard", { replace: true });
Return Value: Promise<boolean> - true if navigation succeeded, false if blocked by guard.
Examples:
// Basic navigation
async function goToUser(userId) {
const success = await router.navigate(`/users/${userId}`);
if (!success) {
console.log("Navigation was blocked");
}
}
// With error handling
async function navigateSafely(path) {
try {
await router.navigate(path);
} catch (error) {
console.error("Navigation failed:", error);
await router.navigate("/error");
}
}
// Conditional navigation
async function submitForm() {
const saved = await saveData();
if (saved) {
await router.navigate("/success");
} else {
await router.navigate("/error", { replace: true });
}
}
Register a global navigation guard that runs before every navigation.
Signature:
const unsubscribe = router.onBeforeEach((to, from) => {
// Return: true | false | "/redirect" | { path, query, ... }
});
Examples:
// Authentication guard
const unsubAuth = router.onBeforeEach((to, from) => {
if (to.meta?.requiresAuth && !isAuthenticated()) {
return { path: "/login", query: { redirect: to.path } };
}
return true;
});
// Role-based access
router.onBeforeEach((to, from) => {
if (to.meta?.roles) {
const userRoles = getCurrentUserRoles();
const hasAccess = to.meta.roles.some(role => userRoles.includes(role));
if (!hasAccess) return "/unauthorized";
}
});
// Confirm navigation away from unsaved changes
router.onBeforeEach((to, from) => {
if (from?.meta?.hasUnsavedChanges && hasUnsavedChanges()) {
const confirmed = confirm("You have unsaved changes. Leave anyway?");
if (!confirmed) return false;
}
});
// Async guard (e.g., check permissions from API)
router.onBeforeEach(async (to, from) => {
if (to.meta?.checkPermission) {
const allowed = await checkPermissionAPI(to.path);
if (!allowed) return "/forbidden";
}
});
Dynamically manage routes at runtime.
Examples:
// Add route dynamically
const removeRoute = router.addRoute({
path: "/admin/new-feature",
component: NewFeaturePage,
meta: { requiresAuth: true }
});
// Remove route when feature is disabled
if (featureDisabled) {
router.removeRoute("/admin/new-feature");
}
// Add routes based on user permissions
async function setupUserRoutes(permissions) {
if (permissions.includes("admin")) {
router.addRoute({ path: "/admin", component: AdminPage });
}
if (permissions.includes("analytics")) {
router.addRoute({ path: "/analytics", component: AnalyticsPage });
}
}
// Cleanup routes on logout
function onLogout() {
router.removeRoute("/admin");
router.removeRoute("/analytics");
router.navigate("/login");
}
Control the router lifecycle.
Examples:
// Basic startup
const router = app.use(Router, { /* config */ });
await router.start();
// Conditional startup
async function initApp() {
const router = app.use(Router, { routes });
// Wait for auth check before starting router
const user = await checkAuthStatus();
if (user) {
setupAuthenticatedRoutes(router, user.permissions);
}
await router.start();
}
// Cleanup on app destroy
async function destroyApp() {
await router.stop();
// Router listeners removed, navigation disabled
}
Access current route information reactively:
app.component("Breadcrumbs", {
setup({ signal }) {
// Access reactive route properties from router
return {
currentPath: app.router.currentRoute,
params: app.router.currentParams,
query: app.router.currentQuery
};
},
template: (ctx) => `
<nav class="breadcrumbs">
<span>Path: ${ctx.currentPath.value?.path || "/"}</span>
${ctx.params.value?.id ? `<span>ID: ${ctx.params.value.id}</span>` : ""}
${ctx.query.value?.tab ? `<span>Tab: ${ctx.query.value.tab}</span>` : ""}
</nav>
`
});
// Watch for route changes
router.currentRoute.watch((route) => {
// Update page title
document.title = route?.meta?.title || "My App";
// Track page view
analytics.pageView(route?.path);
});
// Conditional rendering based on route
app.component("Navigation", {
setup() {
return { route: app.router.currentRoute };
},
template: (ctx) => `
<nav>
<a href="#/" class="${ctx.route.value?.path === "/" ? "active" : ""}">Home</a>
<a href="#/about" class="${ctx.route.value?.path === "/about" ? "active" : ""}">About</a>
</nav>
`
});
router.onError((error, to, from) => {
console.error("Navigation error:", error.message);
console.error("Failed navigation:", from?.path, "→", to?.path);
// Handle specific errors
if (error.message.includes("not found")) {
router.navigate("/404");
} else if (error.message.includes("network")) {
router.navigate("/offline");
}
});
Replace the default error handling behavior:
router.setErrorHandler({
// Called for critical errors (throws)
handle(error, context, details = {}) {
console.error(`[Router Error] ${context}:`, error.message, details);
// Send to error tracking service
errorTracker.capture(error, { context, details });
// Re-throw formatted error
throw new Error(`Router error in ${context}: ${error.message}`);
},
// Called for warnings (logs only)
warn(message, details = {}) {
console.warn(`[Router Warning] ${message}`, details);
},
// Called for non-critical errors (logs only)
log(message, error, details = {}) {
console.error(`[Router] ${message}`, error, details);
errorTracker.capture(error, { message, details });
}
});
router.emitter.on("router:onError", (error, to, from) => {
// Log to analytics
analytics.trackError({
type: "navigation_error",
message: error.message,
to: to?.path,
from: from?.path
});
});
| Error Type | Detection | Recovery |
|---|---|---|
| Network/Import failure | error.message.includes("fetch") |
Show offline page, retry |
| Route not found | error.message.includes("not found") |
Show 404 page |
| Guard rejection | Guard returns false |
Stay on current page |
| Guard error | Guard throws | Navigate to error page |
| Component error | Try/catch in setup | Show error boundary |
| Permission denied | Guard returns redirect | Navigate to login/unauthorized |
Extend router functionality with reusable plugins.
const MyPlugin = {
// REQUIRED: Unique plugin name
name: "my-plugin",
// OPTIONAL: Plugin version
version: "1.0.0",
// REQUIRED: Called when plugin is installed
install(router, options = {}) {
// Access router API
// Register event handlers
// Add custom functionality
},
// OPTIONAL: Called when router is destroyed
destroy(router) {
// Cleanup resources
}
};
// Install with options
router.use(MyPlugin, { option1: "value1" });
// Get all plugins
const plugins = router.getPlugins();
// [{ name: "my-plugin", version: "1.0.0", ... }]
// Get specific plugin
const myPlugin = router.getPlugin("my-plugin");
// Remove plugin
router.removePlugin("my-plugin");
const AnalyticsPlugin = {
name: "analytics",
version: "1.0.0",
install(router, options = {}) {
const { trackingId, debug = false } = options;
// Initialize analytics
if (trackingId) {
initAnalytics(trackingId);
}
// Track page views
router.emitter.on("router:afterEach", (to, from) => {
if (debug) {
console.log("[Analytics] Page view:", to.path);
}
trackPageView({
path: to.path,
title: to.meta.title,
referrer: from?.fullUrl
});
});
// Track errors
router.emitter.on("router:onError", (error, to, from) => {
trackException({
description: error.message,
fatal: false
});
});
}
};
// Usage
router.use(AnalyticsPlugin, {
trackingId: "UA-123456-1",
debug: process.env.NODE_ENV === "development"
});
const PageTitlePlugin = {
name: "page-title",
version: "1.0.0",
install(router, options = {}) {
const {
defaultTitle = "My App",
separator = " | ",
suffix = ""
} = options;
router.emitter.on("router:afterEach", (to) => {
const pageTitle = to.meta.title;
if (pageTitle) {
document.title = pageTitle + separator + suffix;
} else {
document.title = defaultTitle;
}
});
}
};
// Usage
router.use(PageTitlePlugin, {
defaultTitle: "My App",
separator: " - ",
suffix: "My App"
});
// Route with meta.title = "Dashboard" → "Dashboard - My App"
// Route without meta.title → "My App"
const AuthPlugin = {
name: "auth",
version: "1.0.0",
install(router, options = {}) {
const {
loginPath = "/login",
unauthorizedPath = "/unauthorized",
isAuthenticated = () => false,
hasRole = () => true,
redirectParam = "redirect"
} = options;
router.emitter.on("router:beforeEach", (context) => {
const { to } = context;
// Check authentication
if (to.meta.requiresAuth && !isAuthenticated()) {
context.redirectTo = {
path: loginPath,
query: { [redirectParam]: to.fullUrl }
};
return;
}
// Check roles
if (to.meta.roles && to.meta.roles.length > 0) {
const hasRequiredRole = to.meta.roles.some(role => hasRole(role));
if (!hasRequiredRole) {
context.redirectTo = unauthorizedPath;
}
}
});
}
};
// Usage
router.use(AuthPlugin, {
loginPath: "/login",
isAuthenticated: () => !!localStorage.getItem("token"),
hasRole: (role) => {
const user = JSON.parse(localStorage.getItem("user") || "{}");
return user.roles?.includes(role);
}
});
The Router plugin includes comprehensive JSDoc type definitions for excellent IDE support.
/** @typedef {import('eleva/plugins').RouteDefinition} RouteDefinition */
/** @typedef {import('eleva/plugins').RouteLocation} RouteLocation */
/** @typedef {import('eleva/plugins').NavigationGuard} NavigationGuard */
/** @typedef {import('eleva/plugins').NavigationTarget} NavigationTarget */
/** @typedef {import('eleva/plugins').RouterOptions} RouterOptions */
/** @typedef {import('eleva/plugins').Router} Router */
// Routing mode
type RouterMode = "hash" | "history" | "query";
// Router configuration
interface RouterOptions {
mode?: RouterMode;
mount: string;
routes: RouteDefinition[];
viewSelector?: string;
globalLayout?: RouteComponent;
queryParam?: string;
onBeforeEach?: NavigationGuard;
}
// Route definition
interface RouteDefinition {
path: string;
component: RouteComponent;
layout?: RouteComponent;
name?: string;
meta?: Record<string, any>;
beforeEnter?: NavigationGuard;
afterEnter?: NavigationHook;
beforeLeave?: NavigationGuard;
afterLeave?: NavigationHook;
}
// Component types
type RouteComponent =
| string // Registered name
| ComponentDefinition // Inline definition
| (() => Promise<{ default: ComponentDefinition }>); // Lazy import
// Current route information
interface RouteLocation {
path: string;
params: Record<string, string>;
query: Record<string, string>;
meta: Record<string, any>;
name?: string;
fullUrl: string;
matched: RouteDefinition;
}
// Navigation guard
type NavigationGuard = (
to: RouteLocation,
from: RouteLocation | null
) => NavigationGuardResult | Promise<NavigationGuardResult>;
type NavigationGuardResult = boolean | string | NavigationTarget | void;
// Navigation target
interface NavigationTarget {
path: string;
params?: Record<string, string>;
query?: Record<string, string>;
replace?: boolean;
state?: Record<string, any>;
}
// Event contexts
interface NavigationContext {
to: RouteLocation;
from: RouteLocation | null;
cancelled: boolean;
redirectTo: string | NavigationTarget | null;
}
interface ScrollContext {
to: RouteLocation;
from: RouteLocation | null;
savedPosition: { x: number; y: number } | null;
}
/** @type {import('eleva/plugins').RouteDefinition[]} */
const routes = [
{
path: "/users/:id",
component: UserPage,
meta: { requiresAuth: true, title: "User Profile" }
}
];
/** @type {import('eleva/plugins').NavigationGuard} */
const authGuard = (to, from) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
return { path: "/login", query: { redirect: to.path } };
}
};
/** @type {import('eleva/plugins').RouterPlugin} */
const myPlugin = {
name: "my-plugin",
version: "1.0.0",
install(router) {
router.onAfterEach((to, from) => {
console.log(to.path);
});
}
};
// File: main.js
import Eleva from "eleva";
import { Router } from "eleva/plugins";
// ============ Components ============
const HomePage = {
template: () => `
<div class="home">
<h1>Welcome</h1>
<nav>
<a href="#/about">About</a>
<a href="#/dashboard">Dashboard</a>
</nav>
</div>
`
};
const AboutPage = {
template: () => `
<div class="about">
<h1>About Us</h1>
<a href="#/">Home</a>
</div>
`
};
const DashboardPage = {
setup(ctx) {
const user = ctx.signal(null);
// Simulate fetching user
setTimeout(() => {
user.value = { name: "John Doe", email: "john@example.com" };
}, 500);
return { user };
},
template: (ctx) => `
<div class="dashboard">
<h1>Dashboard</h1>
${ctx.user.value
? `<p>Welcome, ${ctx.user.value.name}!</p>`
: '<p>Loading...</p>'
}
</div>
`
};
const LoginPage = {
setup(ctx) {
const login = () => {
localStorage.setItem("token", "demo-token");
const redirect = ctx.router.currentQuery.value.redirect || "/dashboard";
ctx.router.navigate(redirect);
};
return { login };
},
template: (ctx) => `
<div class="login">
<h1>Login</h1>
<button @click="login">Login</button>
</div>
`
};
const NotFoundPage = {
template: (ctx) => `
<div class="not-found">
<h1>404 - Not Found</h1>
<p>Path: ${ctx.router.currentParams.value.pathMatch}</p>
<a href="#/">Go Home</a>
</div>
`
};
// ============ Router Setup ============
const app = new Eleva("myApp");
const isAuthenticated = () => !!localStorage.getItem("token");
const router = app.use(Router, {
mode: "hash",
mount: "#app",
routes: [
{ path: "/", component: HomePage },
{ path: "/about", component: AboutPage },
{ path: "/login", component: LoginPage },
{
path: "/dashboard",
component: DashboardPage,
meta: { requiresAuth: true, title: "Dashboard" }
},
{ path: "*", component: NotFoundPage }
],
onBeforeEach: (to, from) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
return { path: "/login", query: { redirect: to.path } };
}
}
});
// Update page title
router.onAfterEach((to) => {
document.title = to.meta.title || "My App";
});
// Start router
router.start().then(() => {
console.log("App ready!");
});
| Vue Router | Eleva Router | Notes |
|---|---|---|
new VueRouter({ routes }) |
app.use(Router, { routes }) |
Plugin-based |
router.push('/path') |
router.navigate('/path') |
Returns Promise |
router.replace('/path') |
router.navigate({ path, replace: true }) |
Via options |
router.go(-1) |
history.back() |
Use native History API |
router.beforeEach(guard) |
router.onBeforeEach(guard) |
Returns unsubscribe |
router.afterEach(hook) |
router.onAfterEach(hook) |
Returns unsubscribe |
$route.params |
router.currentParams.value |
Signal-based |
$route.query |
router.currentQuery.value |
Signal-based |
$route.meta |
router.currentRoute.value.meta |
Via currentRoute |
<router-link to="/path"> |
<a href="#/path"> |
Native links (hash) |
<router-view> |
viewSelector option |
Configure in options |
| React Router | Eleva Router | Notes |
|---|---|---|
<BrowserRouter> |
mode: "history" |
Via options |
<HashRouter> |
mode: "hash" |
Default mode |
useNavigate() |
ctx.router.navigate() |
From context |
useParams() |
router.currentParams.value |
Signal-based |
useLocation() |
router.currentRoute.value |
Full location |
useSearchParams() |
router.currentQuery.value |
Signal-based |
<Route element={<Page />}> |
{ path, component: Page } |
Route definition |
<Outlet> |
viewSelector option |
Layout slot |
loader function |
beforeEnter guard |
Route-level |
errorElement |
onError hook |
Global |
[ElevaRouter] Mount element "#app" was not found in the DOM.
Solution: Ensure the mount element exists before calling router.start():
// Wait for DOM
document.addEventListener("DOMContentLoaded", async () => {
await router.start();
});
Error: Component "MyPage" not registered.
Solution: Register components before defining routes:
app.component("MyPage", MyPageDefinition);
// Then define routes
Solution: Configure server fallback (see Configuration - History Mode).
Solution: Ensure guard returns a value:
// Wrong - no return
router.onBeforeEach((to) => {
if (!isAuth) "/login"; // Missing return!
});
// Correct
router.onBeforeEach((to) => {
if (!isAuth) return "/login";
});
Correct behavior. URL parameters are always strings. Convert as needed:
const id = parseInt(router.currentParams.value.id, 10);
// Log all navigation events
const events = [
"router:beforeEach", "router:beforeResolve", "router:afterResolve",
"router:beforeRender", "router:afterRender", "router:scroll",
"router:afterEnter", "router:afterLeave", "router:afterEach"
];
events.forEach(event => {
router.emitter.on(event, (...args) => {
console.log(`[${event}]`, ...args);
});
});
Eleva uses render batching via queueMicrotask to optimize performance. This means DOM updates happen asynchronously after navigation.
await router.navigate("/users/123");
console.log(document.querySelector('h1').textContent); // May show OLD content!
// To read updated DOM, wait for the next microtask:
await router.navigate("/users/123");
queueMicrotask(() => {
console.log(document.querySelector('h1').textContent); // Now shows NEW content
});
test("navigates to user page", async () => {
await router.navigate("/users/123");
// Wait for batched render
await new Promise(resolve => queueMicrotask(resolve));
expect(document.querySelector('.user-page')).not.toBeNull();
});
Navigation guards run synchronously before rendering, but the actual DOM update is still batched:
router.onAfterEach((to, from) => {
// Navigation completed, but DOM may not have updated yet
console.log("Navigated to:", to.path);
// Use queueMicrotask if you need to access the new DOM
queueMicrotask(() => {
document.querySelector('.page').focus();
});
});
// Only the last navigation will take effect
router.navigate("/page1");
router.navigate("/page2");
router.navigate("/page3"); // This one wins
The Eleva Router Plugin provides:
For questions or issues, visit the GitHub repository.
| ← Back to Lazy Loading | Back to Router Overview | Store Plugin → |