Specifics
@nanoviews/router re-exports everything from @nano_kit/router and adds the parts that render. Most of them come in two forms, like in the other integrations of Nano Kit: one that takes the navigation or the page as an argument, and one that reads them from the injection context. Navigation, URL state, guards, scroll and head descriptors are the core of the router, covered in Nano Kit’s router docs.
Setup with Dependency Injection
Section titled “Setup with Dependency Injection”Getting Started sets the router up in module scope. With dependency injection, the entry of the app provides the navigation, the page and the pages, and renders App:
import { provide } from 'nanoviews/store'import { mount, context$ } from 'nanoviews'import { App, browserNavigation, router, LocationNavigation$, Page$, Pages$ } from '@nanoviews/router'import { routes } from './routes.js'import { pages } from './pages.js'
declare module '@nano_kit/router' { interface AppContext { routes: typeof routes }}
const locationNavigation = browserNavigation(routes)const [$location] = locationNavigationconst $page = router($location, pages)
mount(() => context$( provide(LocationNavigation$, locationNavigation), provide(Page$, $page), provide(Pages$, pages))( App()), document.querySelector('#app')!)The routes and the pages live in modules of their own, so that the other modules import them without importing the app. AppContext types the routes for everything read from the context, so Link({ to: 'user' }) asks for the params of user. The tokens are covered in Nano Kit’s Dependency Injection, and Page$ and Pages$ hold Nanoviews pages here.
router
Section titled “router”The router of this package is the one of @nano_kit/router for Nanoviews pages: a page is a component or a function that returns a child, and layouts are composed around their pages with Outlet.
import { div, component$ } from 'nanoviews'import { browserNavigation, router, page, notFound } from '@nanoviews/router'
const [$location, navigation] = browserNavigation({ home: '/', about: '/about'})const HomePage = component$(() => div()('Home'))const AboutPage = component$(() => div()('About'))
const $page = router($location, [ page('home', HomePage), page('about', AboutPage), notFound(() => div()('Not found'))])pageView
Section titled “pageView”pageView renders the page that router matched, without dependency injection. App is its counterpart that reads Page$.
import { mount } from 'nanoviews'import { pageView } from '@nanoviews/router'
mount(() => pageView($page), document.querySelector('#app')!)// <div>Home</div>
navigation.push('/about') // <div>About</div>navigation.push('/contacts') // <div>Not found</div>The router keeps a page uncalled, and pageView and Outlet build it in the injection context and the scope of the place it renders in. With no page matched and no notFound, pageView renders nothing.
A page is built anew when another page is matched, and stays as it is when only the params change: /users/1 and /users/2 are the same page, so a page reads its params through signals, such as a param. Two routes mapped to the same component, or to one shared loadable, in the same layout keep the page too, state included. Two loadable() calls of one module build it anew on the first switch.
App renders the page of Page$, the way pageView renders an accessor of one: it is what goes into mount, inside the context$ of Setup with Dependency Injection. The navigation, the link extensions and the head sync go into the outermost layout, declared in the pages like any layout:
import { main, nav, component$ } from 'nanoviews'import { Link, Outlet, enableLinkComponentPreload$, enableLinkComponentAriaCurrent$, syncHead$ } from '@nanoviews/router'
export default component$(() => { enableLinkComponentPreload$(true) enableLinkComponentAriaCurrent$() syncHead$()
return ( main()( nav()( Link({ to: 'home' })( 'Home' ), Link({ to: 'user', params: { id: 1 } })( 'Profile' ) ), Outlet() ) )})import { page, layout, loadable } from '@nanoviews/router'import MainLayout from './layouts/Main.js'
export const pages = [ layout(MainLayout, [ page('home', loadable(() => import('./pages/Home.js'))), page('user', loadable(() => import('./pages/User.js'))) ])]At / the navigation is <nav><a href="/" aria-current="page">Home</a><a href="/users/1">Profile</a></nav>, and hovering Profile loads the chunk of the user page. Like effect$, the functions ending with $ are called in a component render, and the enable…$ ones before the links they extend are built.
Outlet
Section titled “Outlet”A layout is a component that renders Outlet where its page goes. It is built once for all of its pages: a navigation between them swaps only the page at the Outlet, nested layouts stay built the same way, and a page outside the layout removes it.
import { main, header, component$, if_ } from 'nanoviews'import { router, page, layout, notFound, Outlet } from '@nanoviews/router'
const AccountLayout = component$(() => ( main()( header()('Account'), if_($signedIn)( () => Outlet(), () => SignInForm() ) )))
const $page = router($location, [ page('home', HomePage), layout(AccountLayout, [ page('profile', ProfilePage), page('security', SecurityPage), notFound(NotFoundPage) ])])Outlet can stand at any depth of the layout, in a component of its own or in a branch built later, like the one here that waits for the user to sign in. A notFound inside a layout renders at its Outlet. Outside of a layout, Outlet throws.
A layout provides nothing but its outlet, so a store that a page injects lives where it would without the layout. With the root context opened above the page, context$()(pageView($page)), a store that a page creates outlives the layout being built anew: a draft typed into an editor page is still there when the user comes back to it.
listenNavigationLinks$ and listenLinks$
Section titled “listenNavigationLinks$ and listenLinks$”These route the clicks on plain anchors, from markup of your own or from a component that renders an a with an href, as long as the component they are called in is mounted:
import { main, component$ } from 'nanoviews'import { listenNavigationLinks$, Outlet } from '@nanoviews/router'
const MainLayout = component$(() => { listenNavigationLinks$(navigation)
return ( main()( Outlet() ) )})With dependency injection, listenLinks$() reads Navigation$ from the context. The clicks left to the browser are the ones a Link leaves, and a click that a Link has handled is not routed twice. listenLinks(navigation) of the core starts at once and returns the function that stops it.
linkComponent and Link
Section titled “linkComponent and Link”linkComponent(navigation, paths, hooks?) creates a Link component for a navigation, and Link is the one for Navigation$ and Paths$ of the context. A link takes a route name in to and its params in params, or any URL in href, builds its href and navigates without a page reload on click:
import { browserNavigation, buildPaths, linkComponent } from '@nanoviews/router'
const routes = { home: '/', user: '/users/:id'} as constconst [, navigation] = browserNavigation(routes)const Link = linkComponent(navigation, buildPaths(routes))
Link({ to: 'user', params: { id: 1 }})( 'Profile')// <a href="/users/1">Profile</a>The props are typed by the routes: params are required for a route with a required param, optional when all of its params are, and not allowed for a route without any, and every other prop but preload is an attribute of the a. Params given as an accessor, params: () => ({ id: $userId() }), keep the href in step, while static ones are read once, when the link is built, which suits a row of a for_ tracked by id. A link routes its clicks with onLinkClick, which leaves to the browser a click with Ctrl, Meta, Shift or Alt held or with a button other than the main one, a click the onClick of the link cancelled, and a click on a link to another origin, or with target set to _blank or _self, a file name in download or rel set to external. Other targets, such as _top, are routed.
The DI Link and a Link from linkComponent are different components: the enable…$ calls extend only the DI one, and the hooks given to linkComponent only its own links. An app uses one of them.
preloadable and enableLinkComponentPreload$
Section titled “preloadable and enableLinkComponentPreload$”These load the chunk of a page, and those of the layouts around it, the first time a link to it is hovered or focused. A link opts in or out with its preload prop, and the second argument, true here, is its default:
import { buildPaths, linkComponent, preloadable } from '@nanoviews/router'import { pages } from './pages.js'
const Link = linkComponent(navigation, buildPaths(routes), [ preloadable(pages, true)])With dependency injection, enableLinkComponentPreload$(true) in the outermost layout does the same for Link, with the pages of Pages$. A link with an href in place of to never preloads.
ariaCurrent and enableLinkComponentAriaCurrent$
Section titled “ariaCurrent and enableLinkComponentAriaCurrent$”These set aria-current="page" on a link to the current location, and follow the location. The pathnames of the link and of the location are compared by default, and the optional predicate decides otherwise, here so that a link to /users is current on /users/1 too:
import { buildPaths, linkComponent, ariaCurrent } from '@nanoviews/router'
const Link = linkComponent(navigation, buildPaths(routes), [ ariaCurrent($location, (url, location) => ( url !== undefined && ( location.pathname === url.pathname || location.pathname.startsWith(`${url.pathname}/`) ) ))])With dependency injection, enableLinkComponentAriaCurrent$(isAriaCurrent?) in the outermost layout does the same for Link, with the location of Location$. A link with an aria-current of its own keeps it.
syncPageHead$ and syncHead$
Section titled “syncPageHead$ and syncHead$”These keep the document head in step with the Head$ of the matched page and of its layouts, as long as the component they are called in is mounted. Call one of them once, in the outermost layout:
import { main, component$ } from 'nanoviews'import { syncPageHead$, Outlet } from '@nanoviews/router'
const MainLayout = component$(() => { syncPageHead$($page)
return ( main()( Outlet() ) )})With dependency injection, syncHead$() reads Page$. Within an injection context, syncHead$ and syncPageHead$ resolve the Head$ factories in it, so a Head$ may inject, and runs once, the first time its page is matched. Without one, syncPageHead$ calls Head$ on every match, and inject in it throws. The Head$ of a layout applies to its pages, and a title, lang or dir of the page wins over the one of the layout. The descriptors are covered in Nano Kit’s Head Management.
title, meta, link and script are head descriptors from @nanoviews/router and element factories from nanoviews, and nanoviews/svg has a title and a script too. A module that needs both aliases one: import { title as headTitle } from '@nanoviews/router'.
Location, Navigation and Paths from the Context
Section titled “Location, Navigation and Paths from the Context”With dependency injection, the rest of the router is injected: Location$, Navigation$ and Paths$, typed by AppContext, and CanGoBack$. Like every inject, they are called in the render, and a handler uses what the render got:
import { inject } from 'nanoviews/store'import { button, component$ } from 'nanoviews'import { Navigation$, CanGoBack$ } from '@nanoviews/router'
const BackButton = component$(() => { const navigation = inject(Navigation$) const $canGoBack = inject(CanGoBack$)
return ( button({ disabled: () => !$canGoBack(), onClick() { navigation.back() } })( 'Back' ) )})URL state is derived in a factory that injects the location:
import { inject } from 'nanoviews/store'import { Location$, param } from '@nanoviews/router'
export function Params$() { const $location = inject(Location$)
return { $userId: param($location, 'id', Number) }}Params$ reads what LocationNavigation$ provides, so the whole app shares its instance, and a page or a Head$ gets the same $userId with inject(Params$).