steeve

Steeve

Posted on April 28, 2024

RiotJS Router

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:

Riot application changing route on Firefox

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>
Enter fullscreen mode Exit fullscreen mode

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 the path attribute corresponds to the current URL path. The path 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>
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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.

Riot application on Google Chrome with a routing showing 404 when the page does not exist

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Or the pages/p-not-found.riot looks like:

<p-not-found>
    <h2> 404 Page Not Found </h2>
</p-not-found>
Enter fullscreen mode Exit fullscreen mode

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'
    }
  ]
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
  • 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> 
Enter fullscreen mode Exit fullscreen mode
  • 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()
}
Enter fullscreen mode Exit fullscreen mode
  • For each component, the current route is passed as Props:
<span is={ r.component } route={ route }></span>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 🍻

💖 💪 🙅 🚩
steeve
Steeve

Posted on April 28, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

RiotJS Router
riotjs RiotJS Router

April 28, 2024