Eleva Router is the official router plugin for Eleva.js, a minimalist, lightweight, pure vanilla JavaScript frontend runtime framework. This plugin provides flexible client-side routing functionality for Eleva.js applications. It supports three routing modes—hash, query, and history—and automatically injects route information (path, query parameters, dynamic route parameters, and full URL) along with a navigation function directly into your component’s setup context.
Version: v1.2.0-alpha
Status: Stable release with enhanced error handling, memory management, and dynamic route parameters support.
The Eleva Router plugin extends Eleva.js with robust client-side routing capabilities. It supports:
#pageName
) for routing.?page=pageName
) where the page
query is used as the route./pageName
) for clean URLs.The plugin automatically injects a route
object and a navigate
function into your component’s setup context so that you can easily access current route information and perform navigation programmatically. With v1.2.0-alpha, the plugin includes configurable view selectors, enhanced error handling, memory management, and support for dynamic route parameters.
/users/:id
) and catch-all wildcards (e.g., /files/:path*
).route
.navigate
function is provided in the context to change routes from within your components.Install via npm:
npm install eleva-router
Or include it directly via CDN:
<!-- jsDelivr (Recommended) -->
<script src="https://cdn.jsdelivr.net/npm/eleva-router"></script>
or
<!-- unpkg -->
<script src="https://unpkg.com/eleva-router"></script>
When installing the plugin via app.use()
, you can pass a configuration object with the following options:
"hash"
(default) – Uses window.location.hash
(e.g., #pageName
)."query"
– Uses window.location.search
with a configurable query parameter (e.g., ?page=pageName
or ?view=pageName
)."history"
– Uses window.location.pathname
with the History API (e.g., /pageName
)."/"
, "/about"
, or dynamic paths like "/users/:id"
).true
): Whether to automatically start the router after plugin installation. If set to false
, you must manually call await app.router.start()
."page"
): The query parameter name used for query mode routing. Only applies when mode
is set to "query"
. Allows customization of the URL parameter name (e.g., "view"
for ?view=about
instead of ?page=about
).Below is an example of setting up Eleva Router with Eleva.js:
import Eleva from "eleva";
import ElevaRouter from "eleva-router";
const app = new Eleva("MyApp");
// Define routed components (no need for separate registration)
const HomeComponent = {
setup: ({ route }) => {
console.log("Home route:", route.path);
return {};
},
template: () => `<div>Welcome Home!</div>`,
};
const AboutComponent = {
setup: ({ route, navigate }) => {
function goHome() {
navigate("/");
}
return { goHome };
},
template: (ctx) => `
<div>
<h1>About Us</h1>
<button @click="goHome">Go Home</button>
</div>
`,
};
const NotFoundComponent = {
setup: ({ route, navigate }) => ({
goHome: () => navigate("/"),
}),
template: (ctx) => `
<div>
<h1>404 - Not Found</h1>
<button @click="goHome">Return Home</button>
</div>
`,
};
// Install the router plugin
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "history", // Can be "hash", "query", or "history"
routes: [
{ path: "/", component: HomeComponent },
{ path: "/about", component: AboutComponent },
],
defaultRoute: { path: "/404", component: NotFoundComponent },
});
// Router starts automatically unless autoStart: false
The router uses an app layout concept where you provide a layout element that contains a dedicated view element for mounting routed components. This allows you to maintain persistent layout elements (like navigation, headers, footers) while only the view content changes during navigation.
The router automatically looks for a view element within your layout using the following selectors (in order of priority, based on selection speed):
#view
- Element with view
id (fastest - ID selector).view
- Element with view
class (fast - class selector)<view>
- Native <view>
HTML element (medium - tag selector)[data-view]
- Element with data-view
attribute (slowest - attribute selector)Note: The difference in selection speed between these selector types is negligible for most practical cases. This ordering is a micro-optimization that may provide minimal performance benefits in applications with very frequent route changes.
You can customize the view element selector by setting the viewSelector
option. This allows you to use your preferred naming convention:
// Using custom view selector
app.use(ElevaRouter, {
layout: document.getElementById("app"),
viewSelector: "router-view", // Custom selector name
routes: [
{ path: "/", component: HomeComponent },
{ path: "/about", component: AboutComponent },
],
});
This configuration will look for elements in this order:
#router-view
- Element with router-view
id.router-view
- Element with router-view
class<router-view>
- Native <router-view>
HTML elementdata-router-view
- Element with data-router-view
attributeExample HTML with custom selector:
<div id="app">
<header>Navigation</header>
<main id="router-view"></main>
<!-- Router will use this -->
<footer>Footer</footer>
</div>
Example HTML Structure:
<div id="app">
<!-- This is your layout element -->
<header>
<nav>
<a href="#/">Home</a>
<a href="#/about">About</a>
</nav>
</header>
<!-- This is your view element - router will mount components here -->
<main data-view></main>
<footer>
<p>© 2024 My App</p>
</footer>
</div>
Alternative View Element Selectors:
<!-- Using ID (highest priority) -->
<div id="app">
<header>...</header>
<main id="view"></main>
<!-- Router will use this -->
<footer>...</footer>
</div>
<!-- Using native <view> element -->
<div id="app">
<header>...</header>
<view></view>
<!-- Router will use this -->
<footer>...</footer>
</div>
<!-- Using class -->
<div id="app">
<header>...</header>
<main class="view"></main>
<!-- Router will use this -->
<footer>...</footer>
</div>
<!-- Using data-view attribute -->
<div id="app">
<header>...</header>
<main data-view></main>
<!-- Router will use this -->
<footer>...</footer>
</div>
<!-- Fallback to layout -->
<div id="app">
<!-- No view element found, router will use this div -->
</div>
For applications that need precise control over router initialization:
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "history",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/about", component: AboutComponent },
],
autoStart: false, // Disable auto-start
});
// Start the router manually when ready
try {
await app.router.start();
console.log("Router started successfully");
} catch (error) {
console.error("Failed to start router:", error);
}
// Clean up when done (e.g., during app shutdown)
await app.router.destroy();
Within any routed component, route information is injected directly into the setup context as route
. For example:
const MyComponent = {
setup: ({ route, navigate }) => {
console.log("Current path:", route.path);
console.log("Query parameters:", route.query);
console.log("Route parameters:", route.params);
console.log("Matched route pattern:", route.matchedRoute);
console.log("Full URL:", route.fullUrl);
// You can also navigate programmatically:
// navigate("/about");
return {};
},
template: (ctx) => `<div>Content here</div>`,
};
Eleva Router supports dynamic route segments using the colon syntax:
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "history",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/users/:id", component: UserDetailComponent },
{ path: "/blog/:category/:slug", component: BlogPostComponent },
{ path: "/files/:path*", component: FileViewerComponent }, // Catch-all parameter
],
});
Access dynamic parameters within your components:
const UserDetailComponent = {
setup: ({ route, navigate }) => {
console.log("User ID:", route.params.id); // e.g., "123" for URL "/users/123"
return {
userId: route.params.id,
goToUser: (id) => navigate("/users/:id", { id }), // Programmatic navigation with params
};
},
template: (ctx) => `
<div>
<h1>User Profile: ${ctx.userId}</h1>
<button @click="goToUser(456)">View User 456</button>
</div>
`,
};
From within a component or externally, you can navigate by calling the navigate
function:
Within a component: Access via the setup context:
// Simple navigation
navigate("/about");
// With route parameters
navigate("/users/:id", { id: 123 });
// Navigation with error handling
try {
await navigate("/dashboard");
} catch (error) {
console.error("Navigation failed:", error);
}
From outside: Use the router instance:
await app.router.navigate("/about");
await app.router.navigate("/users/:id", { id: 123 });
Properly manage the router lifecycle for optimal performance:
// Check if router is running
console.log("Router is active:", app.router.isStarted);
// Clean up the router when your application shuts down
await app.router.destroy();
// Add cleanup to page unload for browser applications
window.addEventListener("beforeunload", async () => {
await app.router.destroy();
});
new Router(eleva, options);
layout
: (HTMLElement) App layout element. Router looks for a view element (#{viewSelector}, .{viewSelector}, <{viewSelector}>, or data-{viewSelector}) within this layout to mount components. Priority based on selection speed (micro-optimization). If no view element is found, the layout itself is used.mode
: (string) “hash” (default), “query”, or “history”.queryParam
: (string) Query parameter name for query mode (default: “page”).viewSelector
: (string) Selector name for the view element (default: “view”).routes
: (array) Array of route objects.defaultRoute
: (object, optional) A fallback route object.async start(): Starts the router by listening for route changes and processing the initial route. Throws an error if startup fails.
try {
await app.router.start();
} catch (error) {
console.error("Router startup failed:", error);
}
async destroy(): Stops the router, cleans up all event listeners, and unmounts the current component. Safe to call multiple times.
await app.router.destroy();
async routeChanged(): Internal method called on URL changes; extracts the current path and query parameters, and mounts the corresponding component. Includes comprehensive error handling.
async navigate(path, params): Programmatically navigates to a given path. Optionally accepts a params object for route parameters.
await app.router.navigate("/users/:id", { id: 123 });
addRoute(route): Dynamically adds a new route. Validates route structure before adding.
app.router.addRoute({
path: "/new-page",
component: NewPageComponent,
props: { title: "New Page" },
});
matchRoute(path): Finds a matching route for the specified path. Returns an object with the matched route and extracted parameters, or null if no match.
const match = app.router.matchRoute("/users/123");
// Returns: { route: {...}, params: { id: "123" } }
route
and navigate
properties directly.install(eleva, options):
Installs the router plugin into an Eleva.js instance. Automatically registers routed components (if provided as definitions), attaches the Router instance to eleva.router
, and optionally starts the router.
The installation process:
queueMicrotask
if autoStart
is trueapp.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "hash",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/contact", component: ContactComponent },
],
defaultRoute: { path: "/404", component: NotFoundComponent },
});
// Default query parameter
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "query",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/services", component: ServicesComponent },
],
});
// URLs: ?page=/, ?page=/services
// Custom query parameter
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "query",
queryParam: "view", // Custom parameter name
routes: [
{ path: "/", component: HomeComponent },
{ path: "/services", component: ServicesComponent },
],
});
// URLs: ?view=/, ?view=/services
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "history",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/about", component: AboutComponent },
],
});
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "history",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/products/:category", component: ProductCategoryComponent },
{ path: "/products/:category/:id", component: ProductDetailComponent },
{ path: "/files/:path*", component: FileViewerComponent }, // Catch-all
],
});
// In your component:
const ProductDetailComponent = {
setup: ({ route }) => {
// For URL "/products/electronics/12345"
console.log(route.params.category); // "electronics"
console.log(route.params.id); // "12345"
return {
category: route.params.category,
productId: route.params.id,
};
},
template: (ctx) => `
<div>
<h1>Product: ${ctx.productId}</h1>
<p>Category: ${ctx.category}</p>
</div>
`,
};
// E-commerce application
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "query",
queryParam: "category",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/electronics", component: ElectronicsComponent },
{ path: "/books", component: BooksComponent },
],
});
// URLs: ?category=/, ?category=/electronics, ?category=/books
// Admin panel
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "query",
queryParam: "section",
routes: [
{ path: "/", component: DashboardComponent },
{ path: "/users", component: UsersComponent },
{ path: "/settings", component: SettingsComponent },
],
});
// URLs: ?section=/, ?section=/users, ?section=/settings
// Content management
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "query",
queryParam: "content",
routes: [
{ path: "/", component: OverviewComponent },
{ path: "/articles", component: ArticlesComponent },
{ path: "/pages", component: PagesComponent },
],
});
// URLs: ?content=/, ?content=/articles, ?content=/pages
// Using "router-view" as the selector
app.use(ElevaRouter, {
layout: document.getElementById("app"),
viewSelector: "router-view",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/about", component: AboutComponent },
],
});
// Using "main" as the selector
app.use(ElevaRouter, {
layout: document.getElementById("app"),
viewSelector: "main",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/about", component: AboutComponent },
],
});
// Using "content" as the selector
app.use(ElevaRouter, {
layout: document.getElementById("app"),
viewSelector: "content",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/about", component: AboutComponent },
],
});
const app = new Eleva("MyApp");
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "history",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/dashboard", component: DashboardComponent },
],
autoStart: false, // Manual control
});
// Start when ready
document.addEventListener("DOMContentLoaded", async () => {
try {
await app.router.start();
console.log("Application routing initialized");
} catch (error) {
console.error("Failed to initialize routing:", error);
}
});
// Clean up on page unload
window.addEventListener("beforeunload", async () => {
await app.router.destroy();
});
const ErrorBoundaryComponent = {
setup: ({ route, navigate }) => {
const handleRetry = () => {
// Navigate back to home or reload current route
navigate("/");
};
return { handleRetry };
},
template: (ctx) => `
<div class="error-boundary">
<h1>Something went wrong</h1>
<button @click="handleRetry">Try Again</button>
</div>
`,
};
app.use(ElevaRouter, {
layout: document.getElementById("app"),
mode: "history",
routes: [
{ path: "/", component: HomeComponent },
{ path: "/about", component: AboutComponent },
],
defaultRoute: { path: "/error", component: ErrorBoundaryComponent },
});
Eleva Router includes comprehensive error handling:
You can implement custom error handling in your components:
const RobustComponent = {
setup: ({ route, navigate }) => {
const safeNavigate = async (path) => {
try {
await navigate(path);
} catch (error) {
console.error("Navigation failed:", error);
// Fallback behavior
await navigate("/");
}
};
return { safeNavigate };
},
template: (ctx) => `
<div>
<button @click="safeNavigate('/risky-route')">Navigate Safely</button>
</div>
`,
};
Eleva Router automatically manages memory to prevent leaks:
Best practices implemented:
destroy()
method when your application shuts downautoStart: false
for applications that need precise initialization timingconst UserComponent = {
setup: ({ route, navigate }) => {
// Validate parameters
const userId = route.params.id;
if (!userId || isNaN(parseInt(userId))) {
navigate("/users"); // Redirect to user list
return {};
}
return { userId };
},
template: (ctx) => `<div>User: ${ctx.userId}</div>`,
};
New Features:
:id
, :path*
)autoStart
optionisStarted
flagBreaking Changes:
Migration Steps:
Update package version:
npm update eleva-router
Add dynamic route parameters (optional):
// Old static routes
{ path: "/user", component: UserComponent }
// New dynamic routes
{ path: "/users/:id", component: UserDetailComponent }
Add error handling (recommended):
// Old way
app.router.navigate("/path");
// New way (recommended)
try {
await app.router.navigate("/path");
} catch (error) {
console.error("Navigation failed:", error);
}
Add cleanup in applications (recommended):
window.addEventListener("beforeunload", async () => {
await app.router.destroy();
});
No action required for most applications - v1.2.0-alpha is designed to be backward compatible with v1.0.x-alpha usage patterns.
Q: What routing modes are supported?
A: You can choose between
"hash"
,"query"
, and"history"
modes via the plugin options.
Q: How do I define dynamic route parameters?
A: Use colon syntax in your route paths (e.g.,
/users/:id
). For catch-all parameters, add an asterisk (e.g.,/files/:path*
).
Q: How do I access route parameters within a component?
A: Route parameters are available in the
route.params
object injected into your component’s setup context.
Q: How do I define a default route?
A: Use the
defaultRoute
option in the plugin configuration to specify a fallback route if no match is found.
Q: How do I customize the query parameter name in query mode?
A: Use the
queryParam
option when installing the router. For example,queryParam: "view"
will use?view=about
instead of?page=about
.
Q: Can I use different query parameters for different applications?
A: Yes, each router instance can have its own
queryParam
setting, allowing multiple routers or applications to use different parameter names.
Q: How do I access route information within a component?
A: Route information is injected directly into the setup context as
route
, and thenavigate
function is also provided.
Q: Can I add routes dynamically after initialization?
A: Yes, use the
addRoute(route)
method on the router instance to add routes dynamically.
Q: How do I handle errors in routing?
A: Wrap navigation calls in try-catch blocks and implement error boundaries using default routes.
Q: When should I use
autoStart: false
?A: Use manual start when you need to ensure certain conditions are met before routing begins, such as user authentication or data loading.
Q: How do I clean up the router?
A: Call
await app.router.destroy()
to clean up event listeners and unmount components.
No Route Matches:
Ensure your routes are correctly defined and that the URL exactly matches one of your route paths or patterns. Check the console for warnings about unmatched routes. If not, the defaultRoute
(if provided) will be used.
Component Not Mounted: Verify that the layout DOM element provided in the options exists and is valid. Ensure the component is properly defined.
Routing Mode Issues:
Double-check that the mode specified in the options is one of "hash"
, "query"
, or "history"
. Check browser console for validation errors.
Navigation Not Working:
Ensure that you are calling navigate()
correctly from the context or via app.router.navigate()
. Check for JavaScript errors in the console.
Route Parameters Not Matching:
Make sure your route pattern follows the correct syntax (e.g., /users/:id
) and that the URL structure matches the pattern.
Memory Leaks:
Ensure you call app.router.destroy()
when your application shuts down. Check for duplicate router instances.
Router Startup Failures: Check the browser console for specific error messages. Ensure the layout element exists when the router starts.
Hash Mode Issues:
In hash mode, ensure you’re not manually manipulating window.location.hash
outside of the router’s navigation methods.
Debug Mode: Enable verbose logging by opening browser console - Eleva Router logs important events and errors for debugging.
Join our community for support, discussions, and collaboration:
This project is licensed under the MIT License.
Thank you for using Eleva Router! I hope this plugin makes building modern, client-side routed applications with Eleva.js a breeze. With v1.1.0’s enhanced stability and new features, you can build more sophisticated routing solutions with confidence.