Router

Elur includes a lightweight client-side router. It supports path parameters, query strings, nested routes, navigation guards, named routes, and history or hash mode.

# Basic routing

createRouter() takes an array of route records and returns a Router. Each record has a path and a component factory:

import { createRouter, RouterView, mount } from "@elurjs/core";
import { ElurComponent, html } from "@elurjs/core";

class Home extends ElurComponent {
  override render() { return html`<h1>Home</h1>`; }
}

class About extends ElurComponent {
  override render() { return html`<h1>About</h1>`; }
}

const router = createRouter([
  { path: "/", component: () => new Home() },
  { path: "/about", component: () => new About() },
]);

class App extends ElurComponent {
  override render() {
    return html`
      <nav>
        <a href="/" @click=${(e: Event) => { e.preventDefault(); router.navigate("/"); }}>Home</a>
        <a href="/about" @click=${(e: Event) => { e.preventDefault(); router.navigate("/about"); }}>About</a>
      </nav>
      <main>${new RouterView()}</main>
    `;
  }
}

mount(new App(), "#app", { router });

RouterView is a class component. new RouterView() renders the matched route's component and updates automatically when the route changes. You can pass a depth (for nested routes) and an explicit router as optional constructor arguments: new RouterView(0, router).

Link is a class component that renders an anchor tag and navigates via the router instead of triggering a full page reload. The constructor takes (to, label) as positional arguments:

import { Link, ElurComponent, html } from "@elurjs/core";

class App extends ElurComponent {
  override render() {
    return html`
      <nav>
        ${new Link("/", "Home")}
        ${new Link("/about", "About")}
      </nav>
    `;
  }
}

# Accessing the router in components

Use elurRouter() to grab the active router from within any component. It's a thin wrapper around inject(RouterKey):

import { ElurComponent, elurRouter, html } from "@elurjs/core";

class Nav extends ElurComponent {
  private router = elurRouter();

  override render() {
    return html`
      <button @click=${() => this.router?.navigate("/about")}>Go to About</button>
    `;
  }
}

# Path parameters

Route patterns with :param segments capture values into router.params:

const router = createRouter([
  { path: "/users/:id", component: () => new UserPage() },
  { path: "/posts/:category/:slug", component: () => new PostPage() },
]);

// In a component:
class UserPage extends ElurComponent {
  private router = elurRouter();

  override render() {
    return html`<p>User ID: ${() => this.router?.params.value.id}</p>`;
  }
}

router.params is a Signal<Record<string, string>> — read it inside a reactive interpolation to update when the route changes.

# Query strings

router.query is a Signal<Record<string, string>>:

class SearchPage extends ElurComponent {
  private router = elurRouter();

  override render() {
    return html`<p>Search: ${() => this.router?.query.value.q}</p>`;
  }
}

You can pass query params when navigating:

router.navigate("/search", { query: { q: "elur", page: 1 } });
// URL: /search?q=elur&page=1
router.navigate("/about");                          // push
router.navigate("/users/42");                       // with params
router.replace("/login");                           // replace (no history entry)
router.navigate("profile", { query: { tab: "info" } }); // with query
router.back();                                      // history back
router.forward();                                   // history forward
router.go(-2);                                      // go back 2 entries
router.isActive("/about");                          // check active route

# Named routes

Give a route a name and navigate to it with params:

const router = createRouter([
  { path: "/users/:id", name: "user", component: () => new UserPage() },
]);

router.navigate({ name: "user", params: { id: 42 } });

# Nested routes

Routes can have children. Paths are joined with the parent:

const router = createRouter([
  {
    path: "/admin",
    component: () => new AdminLayout(),
    children: [
      { path: "", component: () => new AdminDashboard() },       // /admin
      { path: "users", component: () => new AdminUsers() },      // /admin/users
      { path: "settings", component: () => new AdminSettings() },// /admin/settings
    ],
  },
]);

Guards run before navigation commits. Return false to cancel, a string to redirect, or void/true to allow:

const router = createRouter([
  { path: "/", component: () => new Home() },
  { path: "/dashboard", component: () => new Dashboard(), beforeEnter: requireAuth },
]);

function requireAuth(to: string, from: string) {
  if (!isLoggedIn()) return "/login"; // redirect
  // returning void or true allows the navigation
}

// Global guards run before every navigation:
router.beforeEach((to, from) => {
  console.log(`Navigating ${from} → ${to}`);
});

// After each navigation commits:
router.afterEach((to, from) => {
  analytics.track(to);
});

Guards can be async — return a Promise that resolves to the guard result:

async function requireAuth(to: string, from: string) {
  const ok = await checkSession();
  if (!ok) return "/login";
}

# Route metadata

Each record can carry arbitrary meta for guards, layouts, or auth checks:

const router = createRouter([
  { path: "/", component: () => new Home(), meta: { public: true } },
  { path: "/admin", component: () => new Admin(), meta: { roles: ["admin"] } },
]);

router.beforeEach((to) => {
  const route = router.resolve(to).route;
  if (route?.meta?.roles && !hasRoles(route.meta.roles)) return "/403";
});

# History vs hash mode

By default the router uses the History API. Switch to hash mode for static hosting without server rewrites:

const router = createRouter(routes, { mode: "hash" });

You can also set a base path (or via a <base> tag in HTML):

const router = createRouter(routes, { base: "/app" });

# Scroll behavior

Control scroll position on navigation with the scrollBehavior option. It receives the to path, from path, and savedPosition (from back/forward navigation) and returns a scroll position or false to skip:

const router = createRouter(routes, {
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition; // restore on back/forward
    if (to.startsWith("/docs")) return { left: 0, top: 0 }; // top on new pages
    return false; // don't scroll
  },
});

# Using Elur Kit's router

If you're using Elur Kit (the meta-framework), file-based routing is built in. Create files under src/app/:

src/app/
├── page.ts              → /
├── about/page.ts        → /about
├── users/[id]/page.ts   → /users/:id
└── docs/[...slug]/page.ts → /docs/*

Each page.ts exports a default component and optionally a load function for data fetching and generateStaticParams for dynamic routes.

ℹ Note

Elur Kit's router also supports client-side navigation with prefetching and View Transitions. See the Kit Overview for more.

# Types

# RouteRecord

interface RouteRecord {
  path: string;
  component: () => ElurTemplate | ElurComponent;
  name?: string;
  children?: RouteRecord[];
  meta?: Record<string, unknown>;
  beforeEnter?: NavigationGuard;
}
interface NavigateOptions {
  query?: Record<string, string | number | boolean | null | undefined>;
  direction?: NavigationDirection;
  animation?: unknown;
}
type NavigationAction = "push" | "replace" | "pop" | "initial";
type NavigationDirection = "forward" | "back" | "root" | "none";
interface NavigationIntent {
  action: NavigationAction;
  direction: NavigationDirection;
  animation?: unknown;
}

# RouterOptions

interface RouterOptions {
  base?: string;
  mode?: RouterMode;
  scrollBehavior?: ScrollBehavior;
}

# ResolvedRoute

interface ResolvedRoute {
  matched: boolean;
  params: Record<string, string>;
  route: RouteRecord | undefined;
}
type NavigationGuard = (
  to: string,
  from: string,
) => boolean | string | void | { redirect: string };

// Return false to cancel, a path string to redirect, or nothing to continue
type NavigationGuardResult = boolean | string | void | { redirect: string };

# AfterEachHook

type AfterEachHook = (to: string, from: string) => void;

# RouterMode

type RouterMode = "history" | "hash";

# ScrollBehavior

type ScrollBehavior = (
  to: string,
  from: string,
  savedPosition: ScrollPosition | null,
) => ScrollPosition | false;

# RouteLocation

type RouteLocation = string | NamedRouteLocation;

interface NamedRouteLocation {
  name: string;
  params?: Record<string, string>;
}

# Router interface

interface Router {
  readonly current: Signal<string>;
  readonly params: Signal<Record<string, string>>;
  readonly query: Signal<Record<string, string>>;
  readonly base: string;
  readonly intent: Signal<NavigationIntent>;
  readonly canGoBack: Signal<boolean>;
  navigate(location: RouteLocation, options?: NavigateOptions): void;
  replace(location: RouteLocation, options?: NavigateOptions): void;
  back(animation?: unknown): void;
  forward(animation?: unknown): void;
  go(delta: number): void;
  isActive(path: string, exact?: boolean): boolean;
  resolve(path: string): ResolvedRoute;
  readonly routes: RouteRecord[];
  beforeEach(guard: NavigationGuard): () => void;
  afterEach(hook: AfterEachHook): () => void;
}