eleva-router

Eleva Router Documentation

License GitHub package.json version Version 100% Javascript Zero Dependencies Minified Size Gzipped Size

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.


Table of Contents


Overview

The Eleva Router plugin extends Eleva.js with robust client-side routing capabilities. It supports:

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.


Features


Installation

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>

Configuration Options

When installing the plugin via app.use(), you can pass a configuration object with the following options:

Routing Modes

Routes

Default Route

Auto-Start Control

Query Parameter Configuration


Usage

Basic Setup

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

App Layout and View Element

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):

  1. #view - Element with view id (fastest - ID selector)
  2. .view - Element with view class (fast - class selector)
  3. <view> - Native <view> HTML element (medium - tag selector)
  4. [data-view] - Element with data-view attribute (slowest - attribute selector)
  5. Falls back to the layout element itself if no view element is found

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.

Custom View Selectors

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:

  1. #router-view - Element with router-view id
  2. .router-view - Element with router-view class
  3. <router-view> - Native <router-view> HTML element
  4. data-router-view - Element with data-router-view attribute
  5. Falls back to the layout element itself

Example 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>&copy; 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>

Manual Router Control

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();

Accessing Route Information

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>`,
};

Dynamic Route Parameters

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>
  `,
};

Programmatic Navigation

From within a component or externally, you can navigate by calling the navigate function:

Router Lifecycle Management

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();
});

API Reference

Router Class

Constructor

new Router(eleva, options);

Core Methods

Route Management

Component Integration

ElevaRouter Plugin Object


Examples

Example: Basic Hash Routing

app.use(ElevaRouter, {
  layout: document.getElementById("app"),
  mode: "hash",
  routes: [
    { path: "/", component: HomeComponent },
    { path: "/contact", component: ContactComponent },
  ],
  defaultRoute: { path: "/404", component: NotFoundComponent },
});

Example: Query Routing

// 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

Example: History Routing

app.use(ElevaRouter, {
  layout: document.getElementById("app"),
  mode: "history",
  routes: [
    { path: "/", component: HomeComponent },
    { path: "/about", component: AboutComponent },
  ],
});

Example: Dynamic Route Parameters

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>
  `,
};

Example: Custom Query Parameters

// 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

Example: Custom View Selectors

// 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 },
  ],
});

Example: Manual Router Control

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();
});

Example: Error Handling

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 },
});

Error Handling & Recovery

Built-in Error Recovery

Eleva Router includes comprehensive error handling:

Custom 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>
  `,
};

Performance & Memory Management

Automatic Cleanup

Eleva Router automatically manages memory to prevent leaks:

Memory Leak Prevention

Best practices implemented:


Best Practices

  1. Always Handle Errors: Wrap navigation calls in try-catch blocks for production applications
  2. Clean Up Resources: Use the destroy() method when your application shuts down
  3. Validate Routes: Ensure route paths are valid strings and components are properly defined
  4. Use Default Routes: Always provide a default route for better user experience
  5. Manual Control: Use autoStart: false for applications that need precise initialization timing
  6. Parameter Validation: Validate route parameters in your components before use
const 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>`,
};

Migration Guide

From v1.0.x to v1.1.x

New Features:

Breaking Changes:

Migration Steps:

  1. Update package version:

    npm update eleva-router
    
  2. Add dynamic route parameters (optional):

    // Old static routes
    { path: "/user", component: UserComponent }
    
    // New dynamic routes
    { path: "/users/:id", component: UserDetailComponent }
    
  3. 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);
    }
    
  4. 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.


FAQ

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 the navigate 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.


Troubleshooting

Debug Mode: Enable verbose logging by opening browser console - Eleva Router logs important events and errors for debugging.


Contribution & Support

Join our community for support, discussions, and collaboration:


License

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.