PLUMA Svelte Router
This is a client-side router that uses history mode. It's super early in developement so expect bugs, changes, and dragons.
Features
- Aprox 3kB gzip
- No dependencies (other than Svelte)
- Uses history API and native links for navigation
- Multiple options to manage scrolling
Demo app: https://pluma-svelte-router-demo.netlify.app/
To install:
npm i pluma-svelte-router
Simple example
App.svelte
<script>
import {RouterView, initRouter} from 'pluma-svelte-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 'pluma-svelte-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>
More complex router config example
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
component
The This 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 'pluma-svelte-router';
import Spinner from './Spnner.svelte';
import {fetchIntialData} from 'api';
let initialData;
onMount(async () => {
initialData = await fetchIntialData();
});
</script>
{#if initialData}
<RouterView/>
{:else}
<Spinner/>
{/if}
Configuring routes
The most basic route must have at least a path
and a component
reference:
{ path: '/about', component: About }
Path paramaters
{ path: '/products/:productId', component: ProductDetail }
Path parameters will be available in the params
object of the currentRoute
store:
<script>
import {currentRoute} from 'pluma-svelte-router';
console.log($currentRoute.params);
</script>
Meta data
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
}
}
Nested routes
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 defaultCharacterList
insideCharacters
/characters/yoda
/characters/han-solo
Composing nested components
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, or when 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, Shell, 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.
Links
In most cases, the recommended approach for navigation is using standard HTML links with the provided actions.
link
action
To trigger route changes use the link
action:
<script>
import {link} from 'pluma-svelte-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
action
To 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 'pluma-svelte-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>
Custom active CSS class
You can define a custom default active CSS class using the activeClass
setting in the router configuration, or in the action settings:
<!-- Use a custom CSS class -->
<a href="/about" use:link use:active={{activeClass: 'is-active'}}>About</a>
aria-current
value
By default, the active
action will add aria-current="page"
on an active link. You can customize this value depending on your use case:
<!-- Use a custom CSS class -->
<a href="/about" use:link use:active={{ariaCurrent: 'location'}}>About</a>
See the MDN docs for more info on the aria-current
attribute.
Programmatic navigation
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 'pluma-svelte-router';
// Navigate to a path
navigate('/about');
// Navigate but replace current history item instead of pushing a new route
navigate({
path: '/about',
replace: true
});
// Navigate and don't scroll to the top
navigate({
path: '/about',
scrollToTop: false
});
// Navigate and scroll to an id
navigate({
path: '/user/settings',
scrollToId: 'password-form'
});
Scrolling
By default, every route change will scroll to the top left of the page. This can be avoided in three ways:
- Set
scrollToTop
tofalse
on the initial configuration of the router. - Add a configuration to the
link
action<a href="/about" use:link={{scrollToTop: false}}>About</a>
. - Set
blockPageScroll
totrue
on a route configuration which will remove the scroll when rendering the route.
Scroll configuration and positions are restored when going back and forward.
How to enable or disable smooth scrolling?
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.
Query string parameters
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 'pluma-svelte-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 'pluma-svelte-router';
function addParams () {
addQueryParamsToUrl({
name: 'Pepito',
food: 'tacos'
});
}
</script>
<button type="button" on:click={addParams}>Add params to query string</button>
onRouteMatch
hook
This 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;
}
}
API
Router configuration options
-
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 default CSS class that will be applied to active links that use theactive
action. -
scrollToTop
determines if the scroll should be set to the top left when transitioning to a new route. The default istrue
. -
manageScroll
if set tofalse
all scrolling features of the router will be ignored. The default istrue
. -
onRouteMatch
a sync function that will be triggered whenever a path matches a route
Route configuration options
-
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 thebody
element by settingoverflow: hidden;
-
meta
and object with values that can be read from hooks or thecurrentRoute
store.
navigate()
options
-
path
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 istrue
. -
scrollToId
scroll to an element with anid
after transitioning to the next route. -
replace
replace the current item in history instead of adding a new one. The default isfalse
. -
addToHistory
add item to history after navigation. The default istrue
.
link
action options
-
scrollToTop
determines if the scroll should be set to the top left after transitioning to the next route. The default istrue
. -
scrollToId
scroll to an element with anid
after transitioning to the next route.
active
action options
-
matchStart
mark a link as active if thehref
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 isactive
. -
ariaCurrent
the value of thearia-current
attribute that will be added to the link if marked as active. The default ispage
.
FAQ
Roadmap
Features that will be implemented in the not-so-distant future:
- Route data cache
- More hooks (probably)
Features that will be implemented for the 1.0.0
release:
- TypeScript
- Code splitting
Features that will not be implemented:
- Nested routers