Skip to content

Simple and modern Vanilla JS router based on the Navigation API & URLPattern API

License

Notifications You must be signed in to change notification settings

xan105/web-vanilla-router

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

60 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

About

Simple and modern Vanilla JS router based on the πŸ“– Navigation API and πŸ“– URLPattern API.

  • Dependency free.
  • Parameterized routes and URL pattern matchers.
  • Handles navigation: just define your routes.
  • Optional "Not-found" handler.
  • Handler redirection: navigating between routes.

πŸ“¦ Scoped @xan105 packages are for my own personal use but feel free to use them.

πŸ€” Curious to see it in real use? This package powers my personal blog.

Example

import { Router } from "@xan105/vanilla-router"

const router = new Router();

router
.on("/", function(ctx){
  // do something
})
.on("/about", async(ctx) => {
  // do something
})

// Parameterized routes
.on("/user/:id", ({ routeParams }) => {
  const { id } = routeParams;
  // do something
})

// Query parameters (eg: /items?name=foo)
.on("/items", ({ searchParams }) => {
  const { name } = searchParams;
  // do something
})

// Handler redirection
.on("/admin", ({ redirect }) => {
  if (!isLoggedIn()){
    redirect("/login");
  }
  // do something
})
.on("/login", () => {
  // Authenticate
})

// Deferred commit (don't immediately update the URL)
.on("/render", async({ event }) => {
  event.scroll()
  await fetch("/foo/bar", { signal: event.signal });
}, { deferredCommit: true })

// Optional "not found" hook
.on(404, () => {
  console.error("not found !");
})

.listen();

Install

npm i @xan105/vanilla-router

πŸ’‘ The bundled library and its minified version can be found in the ./dist folder.

Via importmap

Create an importmap and add it to your html:

    <script type="importmap">
    {
      "imports": {
        "@xan105/vanilla-router": "./node_modules/@xan105/vanilla-router/dist/router.min.js"
      }
    }
    </script>
    <script type="module">
      import { Router } from "@xan105/vanilla-router"
      const router = new Router();
      router
      .on("/path/to/route", () => {
        // Do a flip()
      })
      .listen();
    </script>
  </body>
</html>

API

⚠️ This module is only available as an ECMAScript module (ESM) and is intended for the browser.

Named export

Router(option?: object): Class

extends πŸ“– EventTarget

Events

error({ detail: { error: string, url: URL } })

This event is dispatched when an error has occured.

will-navigate({ detail: { url: URL } })

This event is dispatched when navigation is about to be intercepted.

did-navigate({ detail: { url: URL } })

This event is dispatched when navigation is done.

Options

  • autoFocus:? boolean (true)

Defines the navigation's focus behavior (automatic or manual).
When enabled the browser will focus the first element with the autofocus attribute, or the element if no element has autofocus set.

  • autoScroll:? boolean (true)

Defines the navigation's scrolling behavior (automatic or manual).
When enabled the browser will handle the scrolling for example restoring the scroll position to the same place as last time if the page is reloaded or a page in the history is revisited.

  • deferredCommit:? boolean (false)

The default behavior of immediately "committing" (i.e., updating location.href and navigation.currentEntry) works well for most situations, but some may find they do not want to immediately update the URL. When deferred commit is used, the navigation will commit when a route's handler fulfills / terminates.

  • autoFire:? boolean (true)

Triggers a navigate event for the current path on a page's first load.
The default behavior is intended for when all requests are routed to your SPA.

Caddy example:

foo.com {
  root * /srv/www/foo.com
  try_files {path} /index.html
  file_server
}

If you are using a "400.html" redirect trick like when hosting on Github's Page. You should not use this and instead handle it yourself.

Example:

404.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script>
      sessionStorage.redirect = location.pathname;
    </script>
    <meta http-equiv="refresh" content="0;URL='/'"></meta>
  </head>
</html>

navigation.js:

const router = new Router({ autoFire: false });
router.on("/", () => { //some route })
router.listen()

const { redirect } = sessionStorage;
delete sessionStorage.redirect;

const url = redirect !== location.pathname ? redirect : "/"
router.navigate(url, { history: "replace" });
  • sensitive?: boolean (true)

Enables case-insensitive route matching when set to false.

  • ignoreAssets?: boolean (true)

Ignore same-origin assets.

When true, if a same-origin URL has a file extension then the navigation won't be intercepted.

  • directoryIndex?: string[] ("index.html")

If a same-origin URL points directly to a directory index file (for example /index.html), the router normalizes it to its directory form (/) internally. This prevents index-file URLs from being treated as asset requests and ensures a single canonical route.

  • manualOverride?: boolean (true)

The router handles when navigation shouldn't be intercepted. But sometimes you just need a manual override!

When true, every navigation triggered by an element with the data-navigation attribute set to false won't be intercepted, eg:

<a href="/some/server/route/" data-navigation="false">Link</a>

Methods

on(path: string | number, handler: (async)function, options?: object): Router

Add a route to the router.

Example:

.on("/foo/bar", (ctx) => {
  //render logic
})

.on("/articles/:id", async({ event, routeParams }) => {
  //render logic
})

A route is unique and has one handler.
Please see the πŸ“– URLPattern API for possible pattern syntax.

You can override some of the router's option per route by passing an option object:

options?: {autoFocus, autoScroll, deferredCommit : boolean }

Please kindly see the corresponding router's options above for more details.

πŸ’‘ The on() method is chainable.

The handler functions is bind to the following arguments:

handler(ctx: { 
  event: NavigateEvent, 
  searchParams: object, 
  routeParams: object,
  redirect: (url: string) => void
})
  • { event: NavigateEvent }

    The corresponding πŸ“– NavigateEvent.
    This exposes the NavigateEvent object instance.

    For example if it makes sense to scroll earlier, you can call event.scroll() πŸ“– NavigateEvent.scroll()

  • { searchParams: object, routeParams: object }

    The query and route parameters represented in key/value pairs.

    // /users/foo/slap
    .on("/users/:id/:action", ({ routeParams }) => {
      console.log(routeParams); //{ id: "foo", action: "slap" }
    })
    
    // /items?foo=bar
    .on("/items", ({ searchParams }) => {
      console.log(searchParams); //{ foo: "bar" }
    })
  • { redirect: (url: string) => void }

    Redirect to the specified URL by aborting the current navigation, navigating to the URL and replacing the current NavigationHistoryEntry (to prevent "back button loop").

    This is a sugar helper function for when you want to redirect from a route handler to another.

    Example

    .on("/foo", ({ redirect }) => { 
      redirect("/bar");
    })
    .on("/bar", () => { 
      console.log("Hello!")
    })

Handling no route found

πŸ’‘ There is a special route 404 that you can optionally add a handler to when you need to handle cases where no match is found.

.on(404, () => { 
  //no match found
})

If no handler is added, the navigation is marked as failed and an error is thrown.

off(path: string | number): Router

Remove a route from the router.

πŸ’‘ The off() method is chainable.

navigate(url: string, options: object): object

Navigate to the specified url.

Short hand to πŸ“– Navigation.navigate().

back(): void | object

Navigates backwards by one entry in the navigation history, if possible.

Returns the object of πŸ“– Navigation.navigate() if a navigation occurs.

forward(): void | object

Navigates forwards by one entry in the navigation history, if possible.

Returns the object of πŸ“– Navigation.navigate() if a navigation occurs.

listen(): Router

Start the router logic by listening to the πŸ“– navigate event and intercept when needed.

πŸ’‘ The listen() method is chainable.

Properties

routes: string[] (read only)

The routers' routes.

current: NavigationHistoryEntry (read only)

Short hand to πŸ“– Navigation.currentEntry.

history: NavigationHistoryEntry[] (read only)

Short hand to πŸ“– Navigation.entries().

updateMetadata(data: {name: string, content:string, details?: object}[]): void

Update the document's metadata: title, description and Open Graph protocol.

Example:

import { Router, updateMetadata } from "@xan105/vanilla-router"

const router = new Router();

router.on("/", () => {
  updateMetadata([
    { name: "title", content: "Xan" },
    { name: "description", content: "Lorem Ipsum" },
    { name: "image", content: "http://localhost/avatar.png" },
    { name: "url", content: "http://localhost" },
    { name: "type", content: "website" }
  ]);
}).listen();

⬇️

<head prefix="og: https://ogp.me/ns# website: https://ogp.me/ns/website#">
  <title>Xan</title>
  <meta name="description" content="Lorem Ipsum" />
  <meta property="og:title" content="Xan" />
  <meta property="og:description" content="Lorem Ipsum" />
  <meta property="og:image" content="http://localhost/avatar.png" />
  <meta property="og:url" content="http://localhost" />
  <meta property="og:type" content="website" />
</head>

πŸ“– The Open Graph protocol

About

Simple and modern Vanilla JS router based on the Navigation API & URLPattern API

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

  •  

Contributors