This article covers creating a Riot app coupled with Riot-Route, Riot's official client-side routing solution.
Before starting, make sure you have a base application running, or read my previous article Setup Riot + BeerCSS + Vite.
Client-side routing links the browser URL with the content on the page. When a user navigates the Riot application, the URL changes without requesting a new front-end from a server. This is called a SPA, for Single Page Applications: Riot handles all data updates and navigation without reloading the page, which makes the app rich and reactive!
Let's create the simplest routing example; then, we will delve into advanced production usage.
Basic Route
We aim to create the following app: a left drawer displaying links to different pages, and when a click happens on a link, the right section prints the corresponding page. The style is powered with the Material Design CSS BeerCSS:
Write the following code in ./index.riot
. The HTML comes from the BeerCSS documentation, and I added RiotJS syntax for the logic:
<index-riot>
<router>
<nav class="drawer left right-round border">
<header>
<nav>
<img class="circle" src="./examples/data/img-card.png"/>
<h6>Jon Snow</h6>
</nav>
</header>
<!-- These links will trigger automatically HTML5 history events -->
<a href="/">
<i>inbox</i>
<span class="max">Inbox</span>
<b>24</b>
</a>
<a href="/favorite">
<i>favorite</i>
<span class="max">Starred</span>
<b>3</b>
</a>
<a href="/sent">
<i>send</i>
<span class="max">Sent</span>
<b>11</b>
</a>
<div class="medium-divider"></div>
<a href="/subscription">
<i>rocket</i>
<span>Subscription</span>
</a>
<a href="/settings">
<i>settings</i>
<span>Settings</span>
</a>
</nav>
<!-- Your application routes will be rendered here -->
<span style="display:block;margin-left:20px;margin-top:20px">
<route path="/"> <h2>Inbox</h2> </route>
<route path="/favorite"> <h2>Starred</h2> </route>
<route path="/sent"> <h2>Sent</h2> </route>
<route path="/subscription"> <h2>Subscription</h2> </route>
<route path="/settings"> <h2>Settings</h2> </route>
</span>
</router>
<script>
import { Router, Route } from '@riotjs/route'
export default {
components: { Router, Route }
}
</script>
</index-riot>
Source Code: https://github.com/steevepay/riot-beercss/blob/main/examples/riot-route/index.basic.riot
This example uses two Components provided by riot-route:
-
Router: The
<router>
wraps the Riot application and automatically detects all the clicks on links that should trigger navigation change. -
Route: The
<route path="/some/route/:params">
renders the page content if thepath
attribute corresponds to the current URL path. Thepath
can accept regex, or parameters, and you can access the current route with the route object:
<route path="/:some/:route/:param"> {JSON.stringify(route.params)} </route>
<route path="/search(.*)">
<!-- Assuming the URL is "/search?q=awesome" -->
{route.searchParams.get('q')}
</route>
Source Code from the Riot-Route documentation
To access the current route in the Javascript section, it is possible to import the route object from '@riotjs/route':
import { Router, Route, route } from '@riotjs/route'
Advanced Route
Let's delve into advanced routing with the following requirements for a front-end:
- Show a 404 page if a URL path does not exist.
- Access query parameters into each page component.
- For each route, display a Riot Component as a page.
- Create a routing configuration file defining all routes, paths, and components.
In the first step, we will create 6 components, one for each page and another for the 404 Not Found
page. Components are located in the pages directory:
pages/p-favorite.riot
pages/p-inbox.riot
pages/p-sent.riot
pages/p-settings.riot
pages/p-subscription.riot
pages/p-not-found.riot
Each component has only one title <h2>
tag, for instance, the pages/p-sent.riot
component looks like this:
<p-sent>
<h2>Sent</h2>
</p-sent>
Or the pages/p-not-found.riot
looks like:
<p-not-found>
<h2> 404 Page Not Found </h2>
</p-not-found>
Then, create a global routing configuration file in the routes.js. The file returns a list of pages, and each Page has a name
, a path
with a long regex, and a corresponding component:
export default [
{
name : 'Inbox',
href : '/',
path : '/(/?[?#].*)?(#.*)?',
component: 'p-inbox',
icon : 'inbox'
},
{
name : 'Starred',
href : '/favorite',
path : '/favorite(/?[?#].*)?(#.*)?',
component: 'p-favorite',
icon : 'favorite'
},
{
name : 'Sent',
href : '/sent',
path : '/sent(/?[?#].*)?(#.*)?',
component: 'p-sent',
icon : 'send',
separator: true
},
{
name : 'Subscription',
href : '/subscription',
path : '/subscription(/?[?#].*)?(#.*)?',
component: 'p-subscription',
icon : 'rocket',
},
{
name : 'Settings',
href : '/settings',
path : '/settings(/?[?#].*)?(#.*)?',
component: 'p-settings',
icon : 'settings'
}
]
Source code: https://github.com/steevepay/riot-beercss/blob/main/examples/riot-route/routes.js
The <route>
Riot component will use the path
attribute. Each regex is composed of 3 parts:
-
/settings
: Path of the page (Required static string) -
(/?[?#].*)
: Query parameters (Optional group) -
(#.*)?
: Fragment, a section within a page (Optional group)
Now, import the routes.js, and all components into the index.riot file: Define components into the components:{}
Riot Object, and load the routes into the state:{}
Object:
<index-riot>
<router>
<nav class="drawer left right-round border">
<header>
<nav>
<img class="circle" src="./examples/data/img-card.png"/>
<h6>Jon Snow</h6>
</nav>
</header>
<!-- Navigation bar created dynamically -->
<template each={ page in state.pages }>
<a href={ page.href }>
<i>{ page.icon }</i>
<span class="max">{ page.name }</span>
</a>
<div if={ page.separator === true } class="medium-divider"></div>
</template>
</nav>
<!-- Your application components/routes will be rendered here -->
<span style="display:block;margin-left:20px;margin-top:20px">
<route each={ page in state.pages } path={ page.path }>
<span is={ page.component } route={ route }></span>
</route>
<p-not-found if={ state.showNotFound } />
</span>
</router>
<script>
import { Router, Route, route, toRegexp, match } from '@riotjs/route';
import pages from './routes.js'
import pInbox from "./pages/p-inbox.riot";
import pFavorite from "./pages/p-favorite.riot";
import pSent from "./pages/p-sent.riot"
import pSettings from "./pages/p-settings.riot"
import pSubscription from "./pages/p-subscription.riot"
import pNotFound from "./pages/p-not-found.riot"
export default {
components: { Router, Route, pInbox, pFavorite, pSent, pSettings, pSubscription, pNotFound },
state: {
pages,
showNotFound: false
},
onMounted (props, state) {
// ROUTING: create a stream on all routes
this.anyRouteStream = route('(.*)')
// ROUTING: check any route change to understand if the not found site should be displayed
this.anyRouteStream.on.value((path) => {
this.update({ showNotFound: !this.state.pages.some(p => match(path.pathname, toRegexp(p?.path))) })
})
},
onUnmounted() {
this.anyRouteStream.end()
}
}
</script>
</index-riot>
Source code: https://github.com/steevepay/riot-beercss/blob/main/examples/riot-route/index.advanced.riot
This code differs a lot compared to the Basic example; here are the major changes:
- To print a component for each page, a loop is created on
state.pages
. Within each<route></route>
, the span HTML elements are used as Riot components by adding the is attribute:
<route each={ page in state.pages } path={ page.path }>
<span is={ page.component } route={ route }></span>
</route>
- Navigation drawer links are also generated thanks to the route configuration, which is accessible with
state.pages
:
<template each={ page in state.pages }>
<a href={ page.href }>
<i>{ page.icon }</i>
<span class="max">{ page.name }</span>
</a>
<div if={ page.separator === true } class="medium-divider"></div>
</template>
- The route object is used on the Javascript part to check if the current URL exists: A stream of routes is created to listen for route changes on the
onMounted () {}
Riot lifecycle. When a route changes, a function checks if the URL matches an existing route:
onMounted (props, state) {
this.anyRouteStream = route('(.*)')
this.anyRouteStream.on.value((path) => {
this.update({ showNotFound: !this.state.pages.some(p => match(path.pathname, toRegexp(p?.path))) })
})
},
onUnmounted() {
// When the component is unmounted, the stream is stopped.
this.anyRouteStream.end()
}
- For each component, the current route is passed as Props:
<span is={ r.component } route={ route }></span>
Within each component, the route is accessible with props.route
, for instance the c-inbox.riot file:
<p-inbox>
<h2> Inbox </h2>
<span>Filter: { props.route.searchParams.get('filter') }</span><br>
<span>Order By: { props.route.searchParams.get('order') }</span>
</p-inbox>
Now you can use query parameters to request an API when the page is loaded in the onMounted(){}
Riot lifecycle.
Conclusion
Riot Route lets you create the navigation easily, with a syntax that is always close to HTML standards.
One limitation of a Single Page Application is that it depends on a Backend/API to load the data. The browser/user has to wait until the content is rendered. To counter this issue, you can render HTML on the server side with Riot-SSR: the first request receives the page filled with data, so the user won't have to wait.
Have a great day! Cheers 🍻