@xan105/vanilla-router
Version:
Simple and modern Vanilla JS router based on the Navigation API and URLPattern API
394 lines (271 loc) ⢠11 kB
Markdown
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 `` 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 /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/)