Skip to content

Latest commit

 

History

History
863 lines (636 loc) · 20 KB

File metadata and controls

863 lines (636 loc) · 20 KB

API Reference

This document describes the public surface exported from @elelcode/lit-router.

Import both the class API and the custom element registration:

import { Router } from "@elelcode/lit-router";
import "@elelcode/lit-router";

Exports

import {
  DEFAULT_CHILD_SLOT,
  type NavigationDirection,
  type ReadonlyRouteDefinition,
  type RouteComponentFactory,
  type RouteContext,
  type RouteContextReceiver,
  type RouteDefinition,
  type RouteErrorDetail,
  type RouteGuard,
  type RouteGuardResult,
  type RouteInsertOptions,
  type RouteLeaveGuard,
  type RouteLoadContext,
  type RouteLoadingDetail,
  type RouteLocation,
  type RouteMeta,
  type RouteNotFoundDetail,
  type RouteParamsInput,
  type RouteParamValue,
  type RouteQueryInit,
  type RouteQueryValue,
  Router,
  type RouterChangeDetail,
  type RouterChangeDetailJson,
  type RouteResolveOptions,
  type RouterEvent,
  type RouterEventListener,
  type RouterEventMap,
  type RouterLinkAttributes,
  type RouterLinkOptions,
  type RouterMode,
  type RouterSlotDetail,
  type RouterSlotDetailJson,
  RouterView,
  type RouteSelector,
  type RouteTreeChangeDetail,
} from "@elelcode/lit-router";

Router

new Router(options?)

Creates a router instance and starts it immediately unless options.autoStart === false.

const router = new Router({
  basePath: "/app",
  routes: [
    { path: "", name: "home", component: "home-page" },
    { path: "settings", name: "settings", component: "settings-page" },
  ],
});

Constructor options:

  • routes?: RouteDefinition[]
  • basePath?: string
  • mode?: "history" | "hash"
  • beforeRoute?: RouteGuard
  • autoStart?: boolean

Notes:

  • basePath defaults to "/".
  • mode defaults to "history". In "hash" mode, route paths live after # (for example /#/app/settings) so static hosts do not need rewrite rules.
  • The router requires the Navigation API for same-document navigations, including hash-mode route changes.
  • Only one started Router instance is supported per document.
  • Use configure() to update router options after construction.

Properties

router.current: RouterChangeDetail

The latest committed route state. Treat it as read-only application state.

router.routes: readonly ReadonlyRouteDefinition[]

Gets the current frozen read-only route tree snapshot.

Assigning router.routes = [...] replaces the full tree and triggers a route refresh.

router.cloneRoutes(): RouteDefinition[]

Returns a mutable cloned snapshot of the current route tree.

router.basePath: string

Current base path. Prefer configure({ basePath }) when changing it at runtime so the router refreshes immediately.

In hash mode, basePath remains part of the application route path, but that path is read from the hash payload. For example, basePath: "/app" matches /#/app/settings instead of /app/settings.

router.mode: RouterMode

Current routing mode. "history" uses the browser pathname. "hash" uses #/path?query#fragment, so the same basePath and route tree can move between history URLs such as /app/settings and hash URLs such as /#/app/settings. If the initial document URL has no route hash, hash mode treats it as the configured base route without rewriting the first URL.

Common URL shapes:

Configuration History URL Hash URL
basePath: "/" root / /#
basePath: "/" route /settings /#/settings
basePath: "/app" root /app /#/app
basePath: "/app" route /app/settings /#/app/settings
WebView entry + basePath: "/app" n/a /index.html#/app/settings

router.beforeRoute?: RouteGuard

Current global enter guard. Prefer configure({ beforeRoute }) when changing it at runtime.

router.lastError?: RouteErrorDetail

The latest route error, if the current state is an error state.

router.lastNotFound?: RouteNotFoundDetail

The latest not-found detail, if the current state is a not-found state.

Methods

router.start(): void

Starts listening to browser navigation and resolves the current URL.

Throws when:

  • URLPattern is not available
  • another router is already started in the same document

router.stop(): void

Stops browser event handling and invalidates in-flight navigation work.

router.configure(options): void

Updates one or more router options and refreshes the current URL once.

router.configure({
  basePath: "/workspace",
  beforeRoute: authGuard,
  routes,
});

Supported fields:

  • routes
  • basePath
  • mode
  • beforeRoute

router.setRoutes(routes): void

Replaces the entire route tree.

Use this when you really want a full tree swap. If you are enabling or disabling features incrementally, prefer insertRoutes() and removeRoute().

router.insertRoutes(routes, options?): void

Inserts one or more route definitions at runtime.

router.insertRoutes([
  { path: "reports", id: "reports-plugin", component: "reports-page" },
]);

router.insertRoutes([
  { path: "audit", id: "settings-audit", component: "audit-page" },
], {
  parentId: "settings-shell",
  index: 1,
});

RouteInsertOptions:

  • parentId?: string
  • parentName?: string
  • index?: number

Rules:

  • Provide either parentId or parentName, not both.
  • Duplicate route id and duplicate route name are rejected.
  • Inserted child routes still require the parent component to expose the child slot.
  • The router immediately re-matches the current URL after insertion.

router.removeRoute(selector): boolean

Removes a runtime route branch by name or by selector object.

router.removeRoute("reports");
router.removeRoute({ id: "reports-plugin" });

selector may be:

  • string: treated as a route name
  • RouteSelector: { id?: string; name?: string }

Returns true when a route was removed, otherwise false.

router.batchRouteUpdates(update): void

Groups multiple tree updates into a single refresh.

router.batchRouteUpdates(() => {
  router.insertRoutes([{
    path: "alpha",
    name: "alpha",
    component: "alpha-page",
  }]);
  router.insertRoutes([{ path: "beta", name: "beta", component: "beta-page" }]);
});

Rules:

  • The callback must stay synchronous.
  • If the callback throws, the router rolls the batch back.
  • A successful batch emits one route-tree-change event with reason: "batch".

router.push(url): void

Pushes a new navigation entry.

Accepted inputs:

  • string
  • URL
  • RouteLocation

router.replace(url): void

Navigates with replace semantics.

Accepted inputs are the same as push().

router.resolveUrl(url): RouterChangeDetail | null

Resolves a string or URL without navigating. Returns null when the URL is outside basePath or no route branch matches. Hash mode accepts both route URLs such as /app/settings/profile and browser URLs such as /#/app/settings/profile.

const match = router.resolveUrl("/settings/profile");

router.resolveNamed(name, options?): RouterChangeDetail | null

Builds a named-route URL and resolves it without navigating.

const match = router.resolveNamed("message", {
  params: { id: "42" },
  query: { tab: "activity" },
});

router.link(location): string

Builds an application-relative href from a named route.

const href = router.link({
  name: "message",
  params: { id: "42" },
  query: { tab: "activity" },
  hash: "summary",
});

Notes:

  • Requires a route name.
  • In hash mode, returns an href with the route encoded after #, for example /#/app/messages/42?tab=activity#summary.
  • A root route in hash mode emits # when basePath is "/", and #/app when basePath is "/app".
  • Supports literal segments, :param, :param(<pattern>), *, and optional ? tokens such as :lang?/docs.
  • Rejects + and * parameter modifiers during reverse routing.

router.linkAttributes(location, options?): RouterLinkAttributes

Builds attributes for a named-route anchor. Use this when JavaScript creates links that need replace semantics.

const attrs = router.linkAttributes(
  { name: "message", params: { id: "42" } },
  { replace: true },
);
// { href: "/messages/42", "data-router-replace": "" }

In hash mode, plain fragments such as #section are left to the browser. Route-looking hashes outside basePath, such as /#/outside when basePath: "/app", are also not intercepted.

Events

Listen on the Router instance:

router.addEventListener("route-change", (event) => {
  console.log(event.detail.localPathname);
});

Router.addEventListener() is typed for the built-in router event names, so known events expose the corresponding CustomEvent.detail type without a local cast.

The exported helper types are:

interface RouterEventMap {
  "route-change": RouterChangeDetail;
  "route-error": RouteErrorDetail;
  "route-loading-start": RouteLoadingDetail;
  "route-loading-end": RouteLoadingDetail;
  "route-not-found": RouteNotFoundDetail;
  "route-tree-change": RouteTreeChangeDetail;
}

type RouterEvent<K extends keyof RouterEventMap> = CustomEvent<
  RouterEventMap[K]
>;
type RouterEventListener<K extends keyof RouterEventMap> = (
  this: Router,
  event: RouterEvent<K>,
) => void;

route-change

Detail type: RouterChangeDetail

Fires after the router commits URL/current state.

route-tree-change

Detail type: RouteTreeChangeDetail

Fires after runtime route-tree changes.

reason is one of:

  • "replace"
  • "insert"
  • "remove"
  • "batch"

routeCount reports the total number of route definitions. routes is the same frozen read-only snapshot exposed by router.routes.

route-loading-start

Detail type: RouteLoadingDetail

Fires before a branch starts lazy loading.

route-loading-end

Detail type: RouteLoadingDetail

Fires after a branch finishes lazy loading, even when the load fails.

route-error

Detail type: RouteErrorDetail

Fires when navigation fails during:

  • guard evaluation
  • lazy loading
  • view commit
  • other internal failures reported as "unknown"

route-not-found

Detail type: RouteNotFoundDetail

Fires when the current URL is inside basePath but no route matches.

<router-view>

<router-view> is the rendering outlet for a Router.

<router-view></router-view>

Registration

The element is registered by importing the package side effect:

import "@elelcode/lit-router";

Properties and attributes

.router: Router | undefined

Attaches the router instance to this outlet.

const outlet = document.querySelector("router-view")!;
outlet.router = router;

When a router is attached, the outlet starts it if needed.

child-slot="route-child"

Configures the slot name used for nested child routes.

Default: DEFAULT_CHILD_SLOT, which is "route-child".

If a route has children, the parent component must expose a matching slot:

<slot name="route-child"></slot>

no-view-transition

Disables View Transitions for this outlet.

routerView.current: RouterChangeDetail

The latest detail committed into this outlet.

Methods

routerView.push(url): void

Imperative passthrough to router.push(url).

routerView.replace(url): void

Imperative passthrough to router.replace(url).

Events

Listen on the outlet when you care about DOM commit completion instead of router state only:

outlet.addEventListener("route-change", (event) => {
  console.log("DOM committed", event.detail.pathname);
});

route-change

Detail type: RouterChangeDetail

Fires after the outlet has:

  • mounted the branch
  • restored scroll
  • moved focus

This is later than Router's route-change.

route-error

Detail type: RouteErrorDetail

Fires when commit fails inside the outlet, or when the attached router emits a route error.

route-not-found

Detail type: RouteNotFoundDetail

Fires when the attached router enters a not-found state.

Fallback slots

Override the built-in fallback UI with light DOM slots:

<router-view>
  <not-found-page slot="404"></not-found-page>
  <route-error-page slot="error"></route-error-page>
</router-view>

Available slots:

  • slot="404"
  • slot="error"

Host data attributes

The outlet host reflects navigation direction:

  • data-transition-direction="forward"
  • data-transition-direction="backward"
  • data-transition-direction="none"

Use this to write direction-aware View Transition CSS.

Focus and scroll behavior

After a successful route commit:

  • the outlet first tries to focus [data-route-focus]
  • otherwise it focuses the viewport itself
  • scroll restoration is tracked per history entry
  • hash scrolling uses document.getElementById() only

If an anchor target lives inside a shadow tree, expose the same id on the host element so lookup stays O(1).

RouteDefinition

interface RouteDefinition {
  id?: string;
  name?: string;
  path: string;
  slot?: string;
  title?: string;
  viewTransitionName?: string;
  component?: string | RouteComponentFactory;
  children?: RouteDefinition[];
  guard?: RouteGuard;
  beforeLeave?: RouteLeaveGuard;
  load?: (context?: RouteLoadContext) => Promise<unknown>;
  meta?: RouteMeta;
  props?: Record<string, unknown>;
}

Field notes:

  • id: stable tree-management identifier for runtime insertion/removal
  • name: stable route name for push({ name }), replace({ name }), and link()
  • path: route segment. Use "" for index routes and "*" for catch-all
  • slot: parent slot name used by this route. Defaults to "route-child". "404" and "error" are reserved for <router-view> fallbacks.
  • title: document title template. :param tokens are expanded on commit
  • viewTransitionName: route-level view-transition-name used by <router-view> when this route is the leaf
  • component: custom-element tag name or factory returning an HTMLElement
  • children: nested route tree rendered through a named slot
  • guard: enter guard for this route
  • beforeLeave: leave guard for this route when its branch segment unloads
  • load: first-entry async loader. Its resolved value is ignored; use it for code splitting or side effects only. Receives { signal } for cancellation.
  • meta: route metadata for guards, events, and route context. It is shallow cloned on input, shallow frozen in read-only snapshots, and never assigned to the rendered DOM element.
  • props: extra properties assigned onto the rendered element with a shallow Object.assign

Matching semantics:

  • routes are compiled with specificity sorting
  • static segments win over params
  • params win over catch-all
  • declaration order does not decide specificity bugs

Navigation and guard types

RouteLocation

interface RouteLocation {
  name: string;
  params?: RouteParamsInput;
  query?: RouteQueryInit;
  hash?: string;
}

RouteResolveOptions

interface RouteResolveOptions {
  params?: RouteParamsInput;
  query?: RouteQueryInit;
  hash?: string;
}

RouterLinkOptions / RouterLinkAttributes

interface RouterLinkOptions {
  replace?: boolean;
}

interface RouterLinkAttributes {
  href: string;
  "data-router-replace"?: "";
}

RouteGuardResult

Route guards and leave guards may return:

  • true
  • undefined
  • false
  • string
  • URL
  • { to: string | URL; replace?: boolean }

Semantics:

  • true or undefined: allow navigation
  • false: block navigation
  • redirect values: redirect immediately

RouteGuard

type RouteGuard = (
  detail: RouterChangeDetail & {
    from: RouterChangeDetail | null;
    router: Router;
    signal: AbortSignal;
  },
) =>
  | boolean
  | void
  | string
  | URL
  | { to: string | URL; replace?: boolean }
  | Promise<
    | boolean
    | void
    | string
    | URL
    | { to: string | URL; replace?: boolean }
  >;

Order:

  • global beforeRoute
  • matched branch guard functions from parent to leaf
  • each guard receives a navigation signal that aborts when a newer navigation supersedes it

RouteLeaveGuard

type RouteLeaveGuard = (
  detail: RouterChangeDetail & {
    to: RouterChangeDetail | null;
    router: Router;
    signal: AbortSignal;
  },
) =>
  | boolean
  | void
  | string
  | URL
  | { to: string | URL; replace?: boolean }
  | Promise<
    | boolean
    | void
    | string
    | URL
    | { to: string | URL; replace?: boolean }
  >;

Order:

  • runs only for the suffix being unloaded
  • evaluated from leaf back toward the shared parent

RouteLoadContext

interface RouteLoadContext {
  signal: AbortSignal;
}

Use signal to cancel work started by load() when a newer navigation supersedes it.

Route context contract

Route elements should expose one of these contracts:

@property({ attribute: false })
accessor routeContext: RouteContext | undefined;

or

setRouteContext(context: RouteContext): void

RouteContext:

interface RouteContext {
  detail: RouterChangeDetail;
  params: Record<string, string>;
  slot: string;
  branch: RouteDefinition[];
}

detail.query is a URLSearchParams. slot is the slot this component was projected into, and branch is the branch for that slot.

Event detail types

RouterChangeDetail

interface RouterChangeDetail {
  pathname: string;
  localPathname: string;
  basePath: string;
  search: string;
  query: URLSearchParams;
  hash: string;
  params: Record<string, string>;
  branch: RouteDefinition[];
  leaf?: RouteDefinition;
  slotBranches?: Record<string, RouteDefinition[]>;
  slotParams?: Record<string, Record<string, string>>;
  slots?: Record<string, {
    branch: RouteDefinition[];
    leaf?: RouteDefinition;
    params: Record<string, string>;
  }>;
  url: URL;
  historyKey: string;
  direction: "forward" | "backward" | "none";
  toJSON?: () => RouterChangeDetailJson;
}

Field notes:

  • pathname: full browser pathname
  • localPathname: pathname relative to basePath
  • branch: matched route branch from root to leaf
  • leaf: matched leaf route
  • slotBranches: matched non-main slot branches, keyed by slot name
  • slotParams: params for each non-main slot branch
  • slots: slot-aware detail map including the main "route-child" slot
  • toJSON: serializes query as a plain object and url as a string for JSON.stringify(detail)
  • historyKey: internal key for per-entry direction and scroll tracking
  • direction: computed navigation direction

RouterSlotDetail

interface RouterSlotDetail {
  branch: RouteDefinition[];
  leaf?: RouteDefinition;
  params: Record<string, string>;
}

RouteLoadingDetail

interface RouteLoadingDetail {
  url: URL;
  branch: RouteDefinition[];
  loadingSlots?: string[];
  pending: number;
  direction: NavigationDirection;
}

pending is the current in-flight load count across the router. loadingSlots contains the slot names whose matched branches need loading for this navigation.

RouteErrorDetail

interface RouteErrorDetail {
  url: URL;
  error: unknown;
  phase: "guard" | "load" | "commit" | "unknown";
  direction: NavigationDirection;
}

RouteNotFoundDetail

interface RouteNotFoundDetail {
  url: URL;
  basePath: string;
  direction: NavigationDirection;
}

RouteTreeChangeDetail

interface RouteTreeChangeDetail {
  reason: "replace" | "insert" | "remove" | "batch";
  routeCount: number;
  routes: readonly ReadonlyRouteDefinition[];
}

Constraints

  • Modern browser only. URLPattern is required.
  • The router is designed for same-document SPA navigation, not SSR.
  • Same-origin anchor interception is limited to the configured basePath.
  • Lazy loads and navigation commits are race-safe.
  • History direction bookkeeping is bounded instead of growing forever.
  • load() is deduplicated per route while a pending import is in flight.