Tiny Router
A tiny URL router for any app;
- Zero dependencies.
- It has good TypeScript support.
- Framework agnostic. Can be used for React, Preact, Vue, Svelte, and vanilla JS.
// router.ts
import { Router, Page } from '@lifeart/tiny-router';
// Types for :params in route templates
interface Routes {
home: void
category: 'categoryId'
post: 'categoryId' | 'id'
}
export const router = new Router<Routes>({
home: '/',
category: '/posts/:categoryId',
post: '/posts/:categoryId/:id'
})
// async data loading per-route
router.addResolver('post', async function(page) {
const request = await fetch(`/api/pages/${page.params.id}`);
const data = await request.json();
return data;
});
// async component loading per-route
router.addResolver('category', async function(page) {
const MyCategory = await import('./components/my-category');
return {
component: MyCategory.default
}
});
// add all routes handler
router.addHandler((page: Page, data: any) => {
console.log({page, data});
});
// router.activeRoute === null
// change router state & resolve route
await router.mount('/posts/42');
router.activeRoute.page.route === 'category';
router.activeRoute.page.path === '/posts/42';
Store in active mode listen for <a>
clicks on document.body
and Back button
in browser.
// components/layout.js
import type { Page } from '@lifeart/tiny-router';
import { router } from './router.ts';
import { tracked } from '@glimmer/tracking';
import NotFoundComponent from './components/pages/not-found.hbs';
class RouteComponent extends Component {
router = router.addHandler((page, data) = this.navigate(page, data))
@tracked page!: Page;
@tracked routeComponent!: Component;
navigate(page: Page, data: Component) {
this.page = page;
this.routeComponent = data.component || NotFoundComponent;
}
}
Stack based navigation:
import { Router } from '@lifeart/tiny-router';
// lets define stack for route
export const router = new Router<Routes>({
"users": '/users',
"users.user": '/users/:user',
"users.user.card": '/users/:user/:card'
})
in this case, users.user.card
route resolution will be started from users
, then users.user
, and at the end users.user.card
;
it mean we could "chain" resolvers:
router.addResolver('users', () => {
return [ { cards: [ { name: "hello" }] }];
});
router.addResolver('users.user', () => {
const users = router.dataForRoute('users');
return users[0]; // { cards: [ { name: "hello" } ] }
});
router.addResolver('users.user.card', () => {
const user = router.dataForRoute('users.user');
return user.cards[0]; // { name: "hello" }
});
by default, resolved data cached by key params, way to invalidate it -
router.unloadRouteData('users');
in this case, this route will reload data
Chained routing example:
// components/layout.js
import type { Page } from '@lifeart/tiny-router';
import { router } from './router.ts';
import { tracked } from '@glimmer/tracking';
import NotFoundComponent from './components/pages/not-found.hbs';
import Component, { hbs } from '@glimmerx/component';
interface IStackedRouter {
stack: { name: string, data: null | unknown }[];
components?: Record<string, Component>
}
const DefaultRoute = hbs`
{{#if @hasChildren}} {{yield}} {{/if}}
`;
class StackedRouter extends Component<IStackedRouter> {
get tail() {
return this.parts.tail;
}
get parts() {
const [ head, ...tail] = this.args.stack;
return {
head, tail
}
}
get components() {
return this.args.components ?? {};
}
get Component() {
return this.model?.component || this.components[this.route] || DefaultRoute;
}
get route() {
return this.parts.head.name;
}
get model() {
return (this.parts.head.data || {}) as Record<string, unknown>;
}
static template = hbs`
{{#if @stack.length}}
<this.Component
@route={{this.route}}
@hasChildren={{this.tail.length}}
@model={{this.model}}
@params={{@params}}
>
<StackedRouter @components={{this.components}} @stack={{this.tail}} @params={{@params}} />
</this.Component>
{{/if}}
`
}
class RouteComponent extends Component {
router = router.addHandler((page, data, stack) = this.navigate(page, data, stack));
@tracked stack = [];
@tracked params = {};
navigate(page: Page, _ : any, stack: [string, any][]) {
this.params = page.params;
this.stack = stack;
}
static template = hbs`
<StackedRoute @params={{this.params}} @stack={{this.stack}} />
`
}
Install
yarn add @lifeart/tiny-router
Usage
See Tiny Router docs about using the router and subscribing to changes in UI frameworks.
Routes
For string patterns you can use :name
for variable parts.
For TypeScript, you need specify interface with variable names, used in routes.
interface Routes {
routeName: 'var1' | 'var2'
}
new Router<Routes>({
routeName: '/path/:var1/and/:var2'
})
URL Generation
Using getPagePath()
avoids hard coding URL in templates. It is better
to use the router as a single place of truth.
import { getPagePath } from '@lifeart/tiny-router'
…
<a href={getPagePath(router, 'post', { categoryId: 'guides', id: '10' })}>
If you need to change URL programmatically you can use openPage
or redirectPage
:
import { openPage, redirectPage } from '@lifeart/tiny-router'
function requireLogin () {
openPage(router, 'login')
}
function onLoginSuccess() {
// Replace login route, so we don’t face it on back navigation
redirectPage(router, 'home')
}
Server-Side Rendering
Router can be used in Node environment without window
and location
.
In this case, it will always return route to /
path.
You can manually set any other route:
if (isServer) {
router.open('/posts/demo/1')
}