DEV Community

Rajasegar Chandran
Rajasegar Chandran

Posted on

Building a Glimmer Router with page.js

This is the second post in the series of building a Router component in Glimmer.js. In the first post we built our Router component from scratch for handling anchor tag clicks, history state changes and so on. In this post we are going to make use of a client-side routing library called page.js for all those boilerplate code.

About page.js

page is a tiny client-side router inspired by the Express router. It has got a lot of features like 404 support, plugin integrations, using history state to cache data and more.

App.js

This is how our template markup is going to look like for the main App component in our project. As you can see, we have omitted all the custom componets like <Link/> and <Route/>. Because all of them seems to be irrelevant here since we are going to use the page library, it can make use of the standard anchor tags in the browser and the route mapping will be done in pure javascript with the page() functions.

<nav>
<ul>
  <li><a href="/">Home</a></li>
  <li><a href="/about">About</a></li>
  <li><a href="/contact">Contact</a></li>
</ul>
</nav>
<Router/>
Enter fullscreen mode Exit fullscreen mode

Router Component

Our Router component will become slim here. Because we are removing the Link and the Route components since their functionality is taken care by page.js. Since page.js is going to take care of handling all the anchor click events we don't need a custom component to render our anchor tags. And the router registry which we created in our first post is also not required becuase page.js has its own way of keeping the route mappings.

Here we are using a modifier called routes which is a function from a file called routes.js where we will delegate all the routing logic to the page.js.

import routes from './routes.js';

import {
  createTemplate,
  setComponentTemplate,
  templateOnlyComponent,
} from '@glimmer/core';

const Router = setComponentTemplate(createTemplate({ routes },`
      <div id="glimmer-router-outlet" {{ routes }}></div>
   `), templateOnlyComponent())

export { Router };
Enter fullscreen mode Exit fullscreen mode

Let's take a look at our routes.js file. First we are importing renderComponent function from @glimmer/core library. Then we are importing the page library here with the default import. The main function takes an element parameter, which is nothing but a reference to the actual DOM element sent by our Glimmer engine when the element is inserted in to the DOM. This acts as our mount point for rendering the various page components into our DOM when a particular route is visited.

routes.js

import { renderComponent } from '@glimmer/core';
import page from 'page';

export default function(element) {
}
Enter fullscreen mode Exit fullscreen mode

This is how we can make use of page library to implement our routing. These are some example patterns from their project readme page.

page('/', index)
page('/user/:user', show)
page('/user/:user/edit', edit)
page('/user/:user/album', album)
page('/user/:user/album/sort', sort)
page('*', notfound)
page()
Enter fullscreen mode Exit fullscreen mode

As you can see the page() function take two parameters, one the URL pattern with all the parent and child routes, and the dynamic segment placeholders. The second parameter is a callback function which handles the routing logic when the particular URL is visited.

Next we are going to use the page function to render our components based on the route. Let's focus on the home page route which takes an URL like /. For this route we are going to render a component called Home from a folder called pages. This is just for convention, to keep things organized like all the page level components are created in the pages folder.

...
export default function(element) {
  page('/', () => {
    import('./pages/Home.js').then(component => {
      element.innerHTML = '';
      renderComponent(component.default, element);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Injecting services

We can also inject services into our components while rendering them. The renderComponent function takes an options object as the second parameter in which you can inject services and set the arguments for the component. Let's inject a service called LocaleService and initialize it with the language code en_US into our Home page component.

...
export default function(element) {

  page('/', showPageLoad, () => {
    import('./pages/Home.js').then(component => {
      element.innerHTML = '';
      renderComponent(component.default, {
        element: element,
        owner: {
          services: {
            locale: new LocaleService('en_US'),
          },
        }
      });
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Page loading indicators

It might take some time to fetch our component code and render them on to the DOM. So we want to show some loading status to our users while the components are being fetched.
With the current setup how we are going to achieve that. The page library takes any number of callbacks for route handling and it will execute them one by one in the order
we are specifying. So we will add one more callback function called pageLoad before our anonymous rendering callback in the routing logic.

function showPageLoad(ctx, next) {
  element.innerHTML = '<h1>Loading page...</h1>';
  next();
}
Enter fullscreen mode Exit fullscreen mode

In this showPageLoad callback we are doing two things. First, we will set our loading indicator html markup in our mount node in the DOM called element. Second we
are using the the next callback function given by page.js to move to the next callback in our routing logic sequence. This will allow us to do different things
even before mounting our page level components on to the DOM.

page('/', showPageLoad, () => {
  import('./pages/Home.js').then(component => {
    element.innerHTML = '';
    renderComponent(component.default, {
      element: element,
      owner: {
        services: {
          locale: new LocaleService('en_US'),
        },
      }
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Finally we have to call the page() method to start routing and listening for anchor clicks and navigation events like history state changes in the browser.


export default function(element) {
...
  page();
}
Enter fullscreen mode Exit fullscreen mode

This is how our final routes.js file is going to look like for handling various routes like /about, /contact, etc., You can see we are using the showPageLoad callback prior to rendering the respective page components.

import { renderComponent } from '@glimmer/core';
import page from 'page';

export default function(element) {

  function showPageLoad(ctx, next) {
    element.innerHTML = 'Loading page...';
    next();
  }

  page('/', showPageLoad, () => {
    import('./pages/Home.js').then(component => {
    element.innerHTML = '';
    renderComponent(component.default, {
      element: element,
      owner: {
        services: {
          locale: new LocaleService('en_US'),
        },
      }
    });
    });
  });
  page('/about',showPageLoad, () => {
    import('./pages/About.js').then(component => {
    element.innerHTML = '';
    renderComponent(component.default, element);
    });
  });
  page('/contact', showPageLoad, () => {
    import('./pages/Contact.js').then(component => {
    element.innerHTML = '';
    renderComponent(component.default, element);
    });
  });

  page();

}
Enter fullscreen mode Exit fullscreen mode

Source code

You can see the full source code for this post in the glimmer-snowpack repository, which is actually a create-snowpack-app template to build Glimmer apps with Snowpack. You can use this template like below:

npx create-snowpack-app my-glimmer-app --template glimmer-snowpack
Enter fullscreen mode Exit fullscreen mode

Please let me know any feedback or queries in the comments section. In the next post in this series, we are going to make use of another client side routing library to build our Router component.

Oldest comments (0)