JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 12
  • Score
    100M100P100Q77909F
  • License MIT

Declarative SPA routing for Web Components with Navigation API support. Zero dependencies, buildless.

Package Exports

  • @wcstack/router
  • @wcstack/router/auto

Readme

@wcstack/router

What if routing was just HTML tags?

Imagine a future where you define your app's navigation structure in markup — nested routes, layouts, typed parameters — all as native HTML elements. No router config objects, no JavaScript ceremony. Just tags that describe where things go.

That's what <wcs-router>, <wcs-route>, and friends explore. One CDN import, zero dependencies, pure HTML syntax.

Features

Basic Features

  • Declarative Routing: Simply list <wcs-route> tags within an HTML <template>. No JS configuration object required.
  • Nested Route Definitions: Intuitively express nested structures like /products/:id.
  • Parameter Support: Supports path parameters (:id).
  • Fallback (404): Handle undefined paths with <wcs-route fallback>.
  • Navigation API Based: Built on the modern standard Navigation API, offering high affinity with native browser behavior.
  • Zero Config / Buildless: Works directly in the browser without bundling.

Unique Features

  • Light DOM Layout System: Defines layout templates in normal DOM (Light DOM) without forcing Shadow DOM. Makes global CSS application and <slot> insertion easy.
  • Typed Parameters: Specify type constraints like :id(int). Automatically converts values to number type.
  • Mixed Layouts & Routes: Freely nest <wcs-layout> within the routing tree, managing layout switching per area purely through HTML structure.
  • Auto-Binding: Automatically injects URL parameters into components using data-bind attribute (supports props, states, attr, and direct property modes).
  • Declarative <head> Management: Declaratively switch title and meta tags for each page using <wcs-head>.

Usage

<wcs-router>
    <template>
        <!-- When path is "/" -->
        <wcs-route path="/">
            <!-- Apply the "main-layout" layout -->
            <wcs-layout layout="main-layout">
                <main-header slot="header"></main-header>
                <main-body>
                    <!-- When path is "/" -->
                    <wcs-route index>
                        <wcs-head>
                            <title>Main Page</title>
                        </wcs-head>
                        <main-dashboard></main-dashboard>
                    </wcs-route>

                    <!-- When path is "/products" (relative paths below top-level) -->
                    <wcs-route path="products">
                        <wcs-head>
                            <title>Product Page</title>
                        </wcs-head>
                        <!-- When path is "/products" -->
                        <wcs-route index>
                            <product-list></product-list>
                        </wcs-route>
                        <!-- When path is "/products/:productId" -->
                        <wcs-route path=":productId">
                            <!-- productItem.props.productId = productId -->
                            <product-item data-bind="props"></product-item>
                        </wcs-route>
                    </wcs-route>
                </main-body>
            </wcs-layout>
        </wcs-route>

        <!-- When path is "/admin" -->
        <wcs-route path="/admin">
            <!-- Apply the "admin-layout" layout -->
            <wcs-layout layout="admin-layout">
                <wcs-head>
                    <title>Admin Page</title>
                </wcs-head>
                <admin-header slot="header"></admin-header>
                <admin-body></admin-body>
            </wcs-layout>
        </wcs-route>

        <!-- When no path matches -->
        <wcs-route fallback>
            <error-404></error-404>
        </wcs-route>
    </template>
</wcs-router>

<wcs-outlet>
    <!-- Build a DOM tree according to the route path and layout and render it here -->
</wcs-outlet>

<!-- "main-layout" layout -->
<template id="main-layout">
    <section>
        <h1> Main </h1>
        <slot name="header"></slot>
    </section>
    <section>
        <slot></slot>
    </section>
</template>

<!-- "admin-layout" layout -->
<template id="admin-layout">
    <section>
        <h1> Admin Main </h1>
        <slot name="header"></slot>
    </section>
    <section>
        <slot></slot>
    </section>
</template>
  • are custom components in your app.
  • The custom elements above must be defined separately (via an autoloader or manual registration).

Reference

Router (wcs-router)

Define routes and layout slots inside a child template tag. A direct child template tag is required. Outputs according to definitions to <wcs-outlet>. Multiple routers can coexist in the same document when each has a distinct basename.

Attribute Description
basename When routing in a subfolder URL, specify the subfolder. Not required if you don’t run in a subfolder.

Route (wcs-route)

Displays children when the route path matches. Match priority is static paths over parameters.

Attribute Description
path For top-level routes, specify an absolute path starting with /. Otherwise, specify a relative path. For parameters, use :paramName. For catch-all, use *. Top-level routes cannot use relative paths.
index Inherits the upper path.
fallback Displayed when no route matches the path.
fullpath Path including parent routes (read-only).
name Identifier.
guard Enables guard handling. Specify the full path to navigate to on guard cancellation.
Property Description
params Matched parameters (strings).
typedParams Matched parameters (converted types).
guardHandler Sets the guard decision function.

Guard decision function type: (toPath: string, fromPath: string) => boolean | Promise<boolean>

GuardHandler (wcs-guard-handler)

Place as a child of <wcs-route> to declaratively define a guard decision function. Export the function as the default export from a <script type="module">. The <wcs-guard-handler> element itself is removed from the DOM after parsing.

<wcs-route path="/dashboard" guard="/login">
  <wcs-guard-handler>
    <script type="module">
      export default function(toPath, fromPath) {
        return document.cookie.includes('session=');
      }
    </script>
  </wcs-guard-handler>
  <dashboard-page></dashboard-page>
</wcs-route>
  • The guard attribute value is the redirect path when the guard cancels navigation
  • If the function returns false, navigation is cancelled and the user is redirected to the guard path
  • The function can return Promise<boolean> for async checks
  • <wcs-guard-handler> placed outside a <wcs-route> is ignored
  • If no <script type="module"> is present, guardHandler is not set

Typed Parameters

By specifying types for path parameters, you can perform value validation and automatic conversion.

Syntax: :paramName(typeName)

<!-- Integer parameter -->
<wcs-route path="/users/:userId(int)">
  <user-detail></user-detail>
</wcs-route>

<!-- Complex parameters -->
<wcs-route path="/posts/:date(isoDate)/:slug(slug)">
  <post-detail></post-detail>
</wcs-route>

Built-in Types:

Type Name Description Example Converted Type
int Integer 123, -45 number
float Floating point number 3.14, -2.5 number
bool Boolean true, false, 0, 1 boolean
uuid UUID v1-5 550e8400-e29b-41d4-a716-446655440000 string
slug Slug (lowercase alphanumeric and hyphens) my-post-title string
isoDate ISO 8601 Date 2024-01-23 Date
any Any string (default) Any string

Retrieving Values:

// Get from the route element
const route = document.querySelector('wcs-route[path="/users/:userId(int)"]');

// Get as string
console.log(route.params.userId);       // "123"

// Get as typed value
console.log(route.typedParams.userId);  // 123 (number)

Behavior:

  • If the value does not match the type, the route will not match (it does not result in an error).
  • If no type is specified, it is treated as any (same as previous behavior).
  • Specifying an unknown type name also falls back to any.

Layout (wcs-layout)

Loads a template, inserts children into <slot>, and writes to <wcs-layout-outlet>. Light DOM supported. External file supported.

Attribute Description
layout The id attribute of the template tag used as the template.
src URL of an external template file.
name Identifier passed to wcs-layout-outlet.
enable-shadow-root Use Shadow DOM in <wcs-layout-outlet>.
disable-shadow-root Use Light DOM in <wcs-layout-outlet>.

Outlet (wcs-outlet)

Displays a DOM tree according to the routing and layout settings. Define it in HTML, or if missing it is created by <wcs-router>.

LayoutOutlet (wcs-layout-outlet)

Displays a DOM tree into <wcs-outlet> according to the layout (<wcs-layout>) settings. Inherits the name attribute from <wcs-layout>. Use the name attribute to identify styling targets.

Attribute Description
name The name attribute of <wcs-layout>. Use it to identify styling targets.

Light DOM Limitations

When utilizing disable-shadow-root (Light DOM), slot replacement targets only direct children of <wcs-layout>. Elements with slot attributes inside <wcs-route> will not be placed in the slot.

<!-- NG: <div slot="header"> is not a direct child of wcs-layout, so it doesn't go into the slot -->
<wcs-layout layout="main" disable-shadow-root>
  <wcs-route path="/page">
    <div slot="header">Header Content</div>
  </wcs-route>
</wcs-layout>

<!-- OK: Make the element with slot attribute a direct child of wcs-layout -->
<wcs-layout layout="main" disable-shadow-root>
  <div slot="header">Header Content</div>
  <wcs-route path="/page">
    <!-- Page content -->
  </wcs-route>
</wcs-layout>

In the case of enable-shadow-root (Shadow DOM), this limitation does not apply because the native <slot> function is used.

Head (wcs-head)

Manages document <head> elements per route. Uses a stack-based system where the most recently connected Head is prioritized.

<wcs-route path="/about">
  <wcs-head>
    <title>About Us</title>
    <meta name="description" content="About our company">
  </wcs-head>
  <about-page></about-page>
</wcs-route>

Supported elements: <title>, <meta>, <link>, <base>, <script>, <style>

Behavior:

  • Captures the initial <head> state on first connection
  • When multiple <wcs-head> elements are active, the last connected one takes priority
  • When all <wcs-head> elements disconnect, the initial state is restored
  • Elements are identified by key (e.g., <meta> by name/property/http-equiv, <link> by rel/href)

Link. Converted to an <a>, and the route path in the to attribute is converted to a URL. When the link path matches the current URL, the active CSS class is automatically added to the generated <a> element.

Attribute Description
to Destination path or URL. Paths starting with / are treated as internal paths (basename is prepended). Other values are treated as external URLs.

Active state: The generated <a> receives the active class when its path matches the current location. Tracking is updated on navigation events (currententrychange, wcs:navigate, popstate).

/* Style active links */
a.active { font-weight: bold; color: blue; }

Auto-Binding (data-bind)

Elements with the data-bind attribute automatically receive matched route parameters. Four binding modes are available:

data-bind value Target Description
"props" element.props Merges params into the props property
"states" element.states Merges params into the states property
"attr" HTML attributes Sets params as HTML attributes via setAttribute()
"" (empty) Direct properties Sets params directly on the element (e.g., element.id = value)
<wcs-route path="/users/:userId(int)">
  <!-- element.props = { userId: 123 } -->
  <user-detail data-bind="props"></user-detail>

  <!-- element.setAttribute("userId", 123) -->
  <div data-bind="attr"></div>
</wcs-route>

Parameters are assigned before connectedCallback fires. For custom elements that are not yet defined, assignment is deferred until customElements.whenDefined() resolves.

Configuration

Initialize the router with optional configuration via bootstrapRouter():

import { bootstrapRouter } from '@wcstack/router';

bootstrapRouter({
  // Custom tag names (all optional)
  tagNames: {
    router: 'wcs-router',       // default
    route: 'wcs-route',         // default
    outlet: 'wcs-outlet',       // default
    layout: 'wcs-layout',       // default
    layoutOutlet: 'wcs-layout-outlet', // default
    link: 'wcs-link',           // default
    head: 'wcs-head'            // default
  },
  // Use Shadow DOM for outlets (default: false)
  enableShadowRoot: false,
  // File extensions stripped from basename (default: [".html"])
  basenameFileExtensions: [".html"]
});

Terminology

  • URL Pathname: location.pathname (e.g. /app/products/42)
  • basename: The app mount path (e.g. /app)
  • internalPath: The routing path inside the app after removing basename (e.g. /products/42)

1) basename specification

1.1 basename resolution order

  1. The basename attribute on <wcs-router basename="/app">
  2. If <base href="/app/"> exists, derive from new URL(document.baseURI).pathname
  3. If neither exists, use empty string "" (assumes running at root)

1.2 basename normalization (important)

basename is always normalized as follows:

  • Add leading / (except empty string)
  • Collapse multiple slashes into one
  • Remove trailing / (except / itself, which is treated as empty)
  • Treat .../index.html or .../*.html as files and remove them
  • If the result is /, basename becomes ""

Examples:

  • "/"""
  • "/app/""/app"
  • "/app/index.html""/app"
  • If basename is "", no <base> exists, and the initial pathname !== "/", it is an error

  • If basename is "/app":

    • "/app" and "/app/" are the same (app root)
    • "/app" matches only "/app" or "/app/..." (does not match "/appX")

2) internalPath specification

2.1 internalPath normalization

internalPath is always treated as an absolute path.

  • Add leading /
  • Collapse multiple slashes
  • Remove trailing / (except root /)
  • If empty, become /
  • In Router normalization, remove trailing *.html when present

Examples:

  • ""/
  • "products"/products
  • "/products/"/products
  • "///a//b/"/a/b

2.2 Get internalPath from URL

Obtain internalPath by matching URL Pathname with basename.

  • If pathname === basename, then internalPath = "/"
  • If pathname starts with basename + "/", then internalPath = pathname.slice(basename.length)
  • Otherwise internalPath = pathname
  • If the slice result is "", then internalPath = "/"

Examples (basename=/app):

  • pathname=/app → internalPath=/
  • pathname=/app/ → internalPath=/
  • pathname=/app/products/42 → internalPath=/products/42

3) <wcs-route path="..."> specification

3.1 path notation

path follows internalPath rules.

  • Root (top-level) is "/"

  • Child routes allow relative paths (recommended)

    • Example: parent /products, child ":id"/products/:id

In implementation, paths are converted to absolute during parsing.

3.2 Matching rules

  • Exact match by segment
  • Parameter :id matches a single segment
  • Catch-all * matches the remaining path (accessible via params['*'])

3.3 Priority (longest match definition)

If multiple candidates exist, pick the higher priority:

  1. More segments
  2. If same, more static segments ("users" > ":id" > "*")
  3. If still same, definition order

Catch-all * has the lowest priority, so more specific routes always take precedence.

Example:

  • /admin/users/:id (static2 + param1)
  • /admin/users/profile (static3) → latter wins

3.4 Trailing slash

  • Matching is done after internal normalization, so

    • /products and /products/ are treated the same (either URL is OK)

3.5 Catch-all (*)

Specify * at the end of a path to match the entire remaining path.

<wcs-route path="/admin/profile"></wcs-route>  <!-- Priority -->
<wcs-route path="/admin/*"></wcs-route>        <!-- Fallback for /admin/xxx -->
<wcs-route path="/*"></wcs-route>              <!-- Last resort -->
Path Match Reason
/admin/profile /admin/profile More segments
/admin/setting /admin/* * matches setting
/admin/a/b/c /admin/* * matches a/b/c
/other /* Top-level catch-all

The matched remaining path is accessible via params['*'].


4.1 When to starts with /

to is treated as internalPath.

  • The actual href is created by joining basename + internalPath
  • Join: "/app" + "/products""/app/products" (no //)

4.2 When to does not start with /

Treated as an external URL (new URL(to) is expected to succeed).

  • Example: https://example.com/

5) “Drop HTML files” is limited

Dropping .html only applies when the pathname actually looks like a file.

  • "/app/index.html""/app" (OK)
  • "/products""/" is NG (do not drop segments)