This is a client-side router for Svelte that uses history mode. It's somewhat stable but expect bugs and changes before the 1.0 release.
Demo app: https://roots-router-demo.netlify.app/
To install:
npm i roots-router
App.svelte
<script>
import {RouterView, initRouter} from 'roots-router';
import Home from './Home.svelte';
import About from './About.svelte';
import Contact from './Contact.svelte';
import Error from './Error.svelte';
import Menu from './Menu.svelte';
const config = {
notFoundComponent: Error,
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/contact', component: Contact }
]
}
initRouter(config);
</script>
<Menu/>
<RouterView/>
Menu.svelte
<script>
import {link, active} from 'roots-router';
</script>
<nav>
<a href="/" use:link use:active>Home</a>
<a href="/about" use:link use:active>About</a>
<a href="/contact" use:link use:active>Contact</a>
</nav>
This example is taken from the demo app.
// Router configuration object
{
notFoundComponent: Error,
onRouteMatch: (from, to) => {
// If the route is not private, just return true and let the router continue
if (!to.meta.isPrivate) return true;
if (isAuthenticated) {
return true;
} else {
navigate({
path: '/login',
replace: true
});
}
},
routes: [
{ path: '/', component: Home },
{ path: '/login', component: Login },
{ path: '/about', component: About },
{ path: '/about/some-modal', components: [About, Modal], blockPageScroll: true },
{ path: '/hello/:name', component: Hello },
{
path: '/nested',
component: Nested,
children: [
{ component: DefaultChild },
{
path: 'child-a',
component: ChildA,
children: [
{component: GrandchildA }
]
},
{ path: 'child-b', component: ChildB }
]
},
{
path: '/private',
component: Private,
meta: {
isPrivate: true
}
},
]
}
this configuration will produce the following available paths:
/
/login
/about
/about/some-modal
/hello/:name
/nested
/nested/child-a
/nested/child-b
/private
RouterView
componentThis component simply renders the current route and nested routes. It does not contain any logic or state. You can freely add it, remove it, or move it around as you see fit. This will not change the state of the router.
For example this could be your App.svelte
component:
<script>
import {onMount} from 'svelte';
import {RouterView} from 'roots-router';
import Spinner from './Spnner.svelte';
import {fetchIntialData} from 'api';
let initialData;
onMount(async () => {
initialData = await fetchIntialData();
});
</script>
{#if initialData}
<RouterView/>
{:else}
<Spinner/>
{/if}
The most basic route must have at least a path
and a component
reference:
{ path: '/about', component: About }
{ path: '/products/:productId', component: ProductDetail }
Path parameters will be available in the params
object of the currentRoute
store:
<script>
import {currentRoute} from 'roots-router';
console.log($currentRoute.params);
</script>
You can also add a meta
object to your routes with custom data. You can read this data from the onRouteMatch
hook, or from the currentRoute
store:
{
path: '/private',
component: Private,
meta: {
isPrivate: true
}
}
You can add nested routes using the children
array:
{
path: '/characters',
component: Characters,
children: [
{ path: '/yoda', component: Yoda },
{ path: '/han-solo', component: HanSolo },
]
}
These routes will produce two available paths:
/characters/yoda
/characters/han-solo
The router will render child routes in the default slot of the parent component:
// Characters.svelte
<h1>Star Wars Characters</h1>
<slot></slot>
When matching the path /characters/yoda
, the Yoda
component will be rendered inside Characters
.
It's possible to add a default first child without a path:
{
path: '/characters',
component: Characters,
children: [
{ component: CharacterList },
{ path: '/yoda', component: Yoda },
{ path: '/han-solo', component: HanSolo },
]
}
Now there will be three paths available:
/characters
which will render the default CharacterList
inside Characters
/characters/yoda
/characters/han-solo
Nested components can be composed right from the router by using the components
array:
{ path: '/some-path', components: [Parent, Child] }
Just as with nested routes, this will render the Child
component in the default slot of the Parent
component.
This feature is useful for using components as layouts, nesting layouts, or integrating modals with the router. For example, when you want deep linking on modals, or you'd like a modal to close when pressing back:
// Layout
{ path: '/home', components: [AppLayout, ShellLayout, Home] },
// Picture modal
{ path: '/photos', components: [Photos] },
{ path: '/photos/:photoId', components: [Photos, PhotoDetailModal], blockPageScroll: true }
See the demo app for an example on using modals that integrate with the router.
In most cases, the recommended approach for navigation is using standard HTML links with the provided actions.
link
actionTo trigger route changes use the link
action:
<script>
import {link} from 'roots-router';
</script>
<!-- Simple navigation -->
<a href="/about" use:link>About</a>
<!-- Navigate without scrolling to the top -->
<a href="/some/nested/path" use:link={{scrollToTop: false}}>Some tab section</a>
<!-- Navigate and then scroll to an element with an id -->
<a href="/user/settings" use:link={{scrollToId: 'password-form'}}>Set your password</a>
The link
action will be totally bypassed on clicks with modifiers (Alt, Control, etc) to maintain native behavior.
active
actionTo highlight an active link use the active
action.
By default, this will add the active
CSS class to the element, but you can configure it to use a different class.
<script>
import {link, active} from 'roots-router';
</script>
<!-- Will mark as active if the router is on /about -->
<a href="/about" use:link use:active>About</a>
<!-- Mark as active if the href also matches the start of the current path eg: /products/123456/reviews -->
<a href="/products" use:link use:active={{matchStart: true}}>Products</a>
You can define a custom default active CSS class using the activeClass
setting in the router configuration, or in the action settings:
<a href="/about" use:link use:active={{activeClass: 'is-active'}}>About</a>
aria-current
valueBy default, the active
action will add aria-current="page"
on an active link. You can customize this value depending on your use case:
<a href="/about" use:link use:active={{ariaCurrent: 'location'}}>About</a>
See the MDN docs for more info on the aria-current
attribute.
Since this router uses the history API, to go back and forward you can simply use:
// Go back
window.history.back();
// Go forward
window.history.forward();
navigate()
import {navigate} from 'roots-router';
// Navigate to a path
navigate('/about');
// Navigate and replace current history item instead of pushing a new route
navigate({
path: '/about',
replace: true
});
// Navigate but don't add the change to the history
navigate({
path: '/about',
addToHistory: false
});
// Navigate but don't scroll to the top
navigate({
path: '/about',
scrollToTop: false
});
// Navigate and scroll to an id afterwards
navigate({
path: '/user/settings',
scrollToId: 'password-form'
});
By default, every route change will scroll to the top left of the page. This can be avoided in three ways:
scrollToTop
to false
on the initial configuration of the router.link
action <a href="/about" use:link={{scrollToTop: false}}>About</a>
.blockPageScroll
to true
on a route configuration which will remove the scroll when rendering the route.Scroll configuration and positions are restored when going back and forward.
This router is agnostic to the scrolling behavior. You should respect a user's prefers-reduced-motion
setting via CSS. See how Boostrap does it for example.
If there are querystring parameters in the URL, you will be able to read them from the query
object of the currentRoute
store:
<script>
import {currentRoute} from 'roots-router';
console.log($currentRoute.query);
</script>
You can also set parameters to the URL without triggering a page change by using the addQueryParamsToUrl
utility function:
<script>
import {addQueryParamsToUrl} from 'roots-router';
function addParams () {
addQueryParamsToUrl({
name: 'Pepito',
food: 'tacos'
});
}
</script>
<button type="button" on:click={addParams}>Add params to query string</button>
onRouteMatch
hookThis router has a single global hook which is triggered when navigate()
is used from the link
action, or from programmatic navigation. The hook won't be triggered when going back or forward.
In your router configuration add a onRouteMatch
sync function. If your hook function returns a truthy value, navigation will continue as usual. If it returns any falsy value, the router will simply stop the navigation request. It's up to you to navigate to another route if you wish to do so.
// Router configuration object
{
onRouteMatch: (from, to) => {
console.log('onRouteMatch:');
console.log('From', from);
console.log('To', to);
// If the route is public, return true and let the router continue doing its thing
if (to.meta.isPublic) return true;
// Or else check if the user is authenticated
if (isAuthenticated()){
return true;
} else {
navigate({
path: '/login',
replace: true
});
}
},
routes: [
]
}
By design, this hook has to be a sync function. If you return a promise it will be ignored. Native promises cannot be cancelled and we didn't want to bloat the router with custom promise cancelation features. We also think a router should be agnostic in this matter.
If you need to perform async logic before entering a route, do so before triggering the route change. This way you'll have total control on how to cancel pending promises if the user triggers a navigation change before the promise has resolved. Then you can do this:
// Router configuration object
{
onRouteMatch: (from, to) => {
cancelPendingPromises();
return true;
}
}
If your application is running in a folder you can configure the basePath
setting in the router options. For example if your app is runing in /some/folder/
you can set:
// Router configuration object
{
basePath: '/some/folder/'
}
The first and last slashes are optional.
Once the router inits, this base path will be added automatically to all available routes. You don't need to add the base path when using the navigate()
function or to the links using the link
action, although if you do, nothing bad will happen.
notFoundComponent
a component reference that will be rendered if there are no matched routes.notFoundComponents
an array of component references that will be rendered if there are no matched routes.activeClass
the CSS class that will be applied to active links that use the active
action. The default is active
.scrollToTop
a boolean that determines if the scroll should be set to the top left when transitioning to a new route. The default is true
.manageScroll
if set to false
all scrolling features of the router will be ignored. The default is true
.onRouteMatch
a sync function that will be triggered whenever a path matches a route.basePath
a base path that will be automatically added to all routes and links using the link
action. The first and last slashes are optional.endWithSlash
a boolean which determines if paths will end with a slash or not. It also affects how the current path in displayed in the URL. The default is false
.path
the path of the route.component
the component that will be rendered when the path is matched.components
the component tree that will be rendered when the path is matched.children
an array of children routes.blockPageScroll
whether to removing the scrolling capability of the body
element by setting overflow: hidden;
.meta
and object with values that can be read from hooks or the currentRoute
store.navigate()
optionspath
the path that will be used to match a route.scrollToTop
determines if the scroll should be set to the top left after transitioning to the next route. The default is true
.scrollToId
scroll to an element with an id
after transitioning to the next route.replace
replace the current item in history instead of adding a new one. The default is false
.addToHistory
add item to history after navigation. The default is true
.link
action optionsscrollToTop
determines if the scroll should be set to the top left after transitioning to the next route. The default is true
.scrollToId
scroll to an element with an id
after transitioning to the next route.active
action optionsmatchStart
mark a link as active if the href
value matches the start of the current path.activeClass
the CSS class that will be applied to the link if marked as active. The default is active
.ariaCurrent
the value of the aria-current
attribute that will be added to the link if marked as active. The default is page
.Features that will be implemented in the not-so-distant future:
Features that will be implemented for the 1.0.0
release:
Features that will not be implemented: