UNPKG

@xan105/vanilla-router

Version:

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

394 lines (271 loc) • 11 kB
About ===== Simple and modern Vanilla JS router based on the šŸ“– [Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API) and šŸ“– [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern). <img align="left" width="89" height="128" src="https://github.com/xan105/web-vanilla-router/raw/main/logo.png"> - 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](https://xan105.com/). Example ======= ```js 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: ```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](https://developer.mozilla.org/en-US/docs/Web/API/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).<br/> When enabled the browser will focus the first element with the autofocus attribute, or the <body> element if no element has autofocus set. - `autoScroll:? boolean` (true) Defines the navigation's scrolling behavior (automatic or manual).<br/> 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.<br/> The default behavior is intended for when all requests are routed to your SPA. [Caddy](https://github.com/caddyserver/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. <details><summary>Example:</summary> **404.html**: ```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**: ```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" }); ``` </details> - `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: ```html <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.<br/> Example: ```js .on("/foo/bar", (ctx) => { //render logic }) .on("/articles/:id", async({ event, routeParams }) => { //render logic }) ``` A route is unique and has one handler.<br/> Please see the šŸ“– [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_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: ```ts handler(ctx: { event: NavigateEvent, searchParams: object, routeParams: object, redirect: (url: string) => void }) ``` - `{ event: NavigateEvent }` The corresponding šŸ“– [NavigateEvent](https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent).<br/> This exposes the NavigateEvent object instance.<br/> For example if it makes sense to scroll earlier, you can call `event.scroll()` šŸ“– [NavigateEvent.scroll()](https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent/scroll) - `{ searchParams: object, routeParams: object }` The query and route parameters represented in key/value pairs. ```js // /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** ```js .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. ```js .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()](https://developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate). #### `back(): void | object` Navigates backwards by one entry in the navigation history, if possible. Returns the object of šŸ“– [Navigation.navigate()](https://developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate#return_value) if a navigation occurs. #### `forward(): void | object` Navigates forwards by one entry in the navigation history, if possible. Returns the object of šŸ“– [Navigation.navigate()](https://developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate#return_value) if a navigation occurs. #### `listen(): Router` Start the router logic by listening to the šŸ“– [navigate](https://developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate_event) 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](https://developer.mozilla.org/en-US/docs/Web/API/Navigation/currentEntry). #### `history: NavigationHistoryEntry[]` (read only) Short hand to šŸ“– [Navigation.entries()](https://developer.mozilla.org/en-US/docs/Web/API/Navigation/entries). ### `updateMetadata(data: {name: string, content:string, details?: object}[]): void` Update the document's metadata: title, description and Open Graph protocol. Example: ```js 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(); ``` ā¬‡ļø ```html <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](https://ogp.me/)