Building a Router component for Glimmer.js
Tags: glimmer, router, routing, glimmerjs
Published: 2020-12-23 10:47:48

This is the first in the series of posts explaining how to implement a routing system for [Glimmer.js](https://glimmerjs.com) apps. In this post we are going to create a `Router` component for Glimmer. Since Glimmer is only a rendering engine, it doesn't have the full-fledged routing capabilities like [Ember.js](https://emberjs.com). Lately I have been playing with the [glimmer-experimental](https://github.com/glimmerjs/glimmer-experimental) libraries for my pet projects, I needed a routing system for those apps. This is my attempt at creating one.

So I have searched for similar implementations with Glimmer and I found out this [realword-example-app](https://github.com/dfreeman/glimmerx-realworld-example-app) by [Dan Freeman](https://github.com/dfreeman), where he conditionally render the components based on the active route manually. I wanted a more refined and automated approach which can be scaled to a lot of routes or pages.

I have been heavily inspired by the Routing libraries used by other JS Frameworks like [React](https://reactjs.org), [Svelte](https://svelte.dev), etc. I wanted to build one for Glimmer. So if you look at how [react-router](https://reactrouter.com/web/guides/quick-start) is implemented, you layout your components and their respective paths inside the `Router` component.

```js
import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
          </ul>
        </nav>
        {/* A <Switch> looks through its children <Route>s and
            renders the first one that matches the current URL. */}
        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

```

In the same manner, Svelte has the [svelte-routing](https://github.com/EmilTholin/svelte-routing) library which is kind of similar.

```html
<!-- App.svelte -->
<script>
  import { Router, Link, Route } from "svelte-routing";
  import Home from "./routes/Home.svelte";
  import About from "./routes/About.svelte";
  import Blog from "./routes/Blog.svelte";

  export let url = "";
</script>

<Router url="{url}">
  <nav>
    <Link to="/">Home</Link>
    <Link to="about">About</Link>
    <Link to="blog">Blog</Link>
  </nav>
  <div>
    <Route path="blog/:id" component="{BlogPost}" />
    <Route path="blog" component="{Blog}" />
    <Route path="about" component="{About}" />
    <Route path="/"><Home /></Route>
  </div>
</Router>
```

So for our Glimmer apps, the routing logic is going to be something like this:

## App.js
```js
import Component, { hbs, tracked } from "@glimmerx/component";
import { Router, Route, Link } from "./GlimmerRouter.js";

import Home from './pages/Home.js';
import About from './pages/About.js';
import Contact from './pages/Contact.js';

import "./App.css";

export default class App extends Component {
  Home = Home;
  About = About;
  Contact = Contact;

  static template = hbs`
    <nav>
      <ul>
        <li><Link @to="/">Home</Link></li>
        <li><Link @to="/about">About</Link></li>
        <li><Link @to="/contact">Contact</Link></li>
      </ul>
    </nav>
    <main>
      <Router>
        <Route @path="/" @component={{this.Home}}/>
        <Route @path="/about" @component={{this.About}}/>
        <Route @path="/contact" @component={{this.Contact}}/>
      </Router>
    </main>
    `;
}
```

We are going to see how we can build each of those components, `Router`, `Route`,  and `Link` using Glimmer components.

## Router Registry
The first thing we need for the routing system to work is a Router registry, which is a collection of route urls and the route object. Every routing system needs a mapping for the url and the component to be mounted when the user visits that url. So we need a registry for the same which stores these mappings. It could be as simple as an array of objects like this:

```json
[
  { path: '/', component: Home },
  { path: '/about', component: About },
]
```

## Route component
The Route component is responsible for mapping a particular path or url to the corresponding Glimmer component. It basically tells the router that when the user is visiting the url it needs to mount the component. It is also responsible for adding a route to the registry.

```js
export class Route extends Component {
  constructor() {
    super(...arguments);
    const route = {
      path: this.args.path,
      component: this.args.component,
    };
    window.routerRegistry.push(route);
  }

  static template = hbs`{{yield}}`;
}
```
## Link component
The next component is a custom `Link` component to basically render the links with anchor elements and a custom class called `glimmer-link` which we will use to bind click events to trap the `onclick` events on the anchor elements to implement client-side navigation.

```js
export class Link extends Component {
  static template = hbs`
  <a href={{@to}} class="glimmer-link">{{yield}}</a>
  `;
}

```
## Router Component
This is the meat of our Routing system where all the actions take place. The `Router` component is responsible for handling the routing events, by listening to the `click` events of our anchor elements generated by our `Link` components and the `popstate` events of the window object whenever there a change in the browser url happens.

It uses the Router registry which we saw earlier and find the matching route objects, in our case, Glimmer components and mount them accordingly in the outlet DOM nodes which is signified by the DOM element with the id `glimmer-router-outlet`.

### Router initialization
In the Router component's constructor we need setup the router registry and event listeners for anchor element click events and `window.popstate` events. And we will start the router to listen for these as soon the `DOMContentLoaded` event fires,it will start navigating to the respective pages.

```js
  constructor() {
    super(...arguments);
    window.routerRegistry = [];
    document.addEventListener("click", this.route.bind(this));
    window.addEventListener("popstate", this.handlePopState.bind(this));
    document.addEventListener("DOMContentLoaded", this.start.bind(this));
  }

```

The `start` function will just redirect to the `/` url which is the home page route.

```js
  start(ev) {
    const path = location.pathname || "/";
    this.navigate(path);
  }
```

### Rendering the components
Next, we need a function called `renderPage` where we will render our components on to the outlet component which is nothing but a div with an id `glimmer-router-outlet`.

```js
  renderPage(component) {
    const outlet = document.getElementById("glimmer-router-outlet");
    outlet.innerHTML = "";
    renderComponent(component, outlet);
  }
```

### Routing to pages on anchor clicks 
Next we will take a look at our `route` function which is the click event handler for our anchor elements. We are listening to `click` events in the window object and filter them out for the anchor tags with the class name `glimmer-link` and then route the page. One thing to note here is to prevent the default behavior of the anchor clicks so that you are not redirected to a different page. We will find the route objects from the registry using the `href` value from the respective anchor tags and match it against the path property of the route objects in the registry. We are also pushing the url to the history state so that our history navigation
with the back and forward buttons work properly.

```js
  route(ev) {
    if (ev.target.classList.contains("glimmer-link")) {
      ev.preventDefault();
      const url = new URL(ev.target.href);
      const [route] = this.registry.filter((r) => r.path === url.pathname);
      if (route) {
        history.pushState({}, "", url);
        this.renderPage(route.component);
      }
    }
  }

```

### Handling window.popstate
Next we will add a function to handle the `window.popstate` events and route to the corresponding pages and
mounting the components accordingly.

```js
  handlePopState(event) {
    const [route] = this.registry.filter((r) => r.path === location.pathname);
    if (route) {
      this.renderPage(route.component);
    }
  }
```

### Programmatic Navigation
We also need our Router object to have capabilities like navigating programmatically to different routes based on the 
application state. This will be useful for Authentication, login and other similar stuff where we need to redirect our
users from Login page to Home page or something like that. This is done in the `navigate` function where in we will pass a `path` parameter like `/about` and using that we find the respective route object and invoke the renderPage function.

```js
  navigate(path) {
    const [route] = this.registry.filter((r) => r.path === path);
    if (route) {
      this.renderPage(route.component);
    }
  }
```

### Tearing down event listeners on component destroy
The last thing we need to do is to cleanup our Router object in the `willDestroy` lifecycle hook by removing all the event listeners we have attached previously in the constructor function.

```js
  willDestroy() {
    document.removeEventListener("click", this.route);
    window.removeEventListener("popstate", this.handlePopState);
  }
```
This is how our final Router component will look like.

```js
export class Router extends Component {
  @tracked registry = window.routerRegistry;

  constructor() {
    super(...arguments);
    window.routerRegistry = [];
    document.addEventListener("click", this.route.bind(this));
    window.addEventListener("popstate", this.handlePopState.bind(this));
    document.addEventListener("DOMContentLoaded", this.start.bind(this));
  }

  renderPage(component) {
    const outlet = document.getElementById("glimmer-router-outlet");
    outlet.innerHTML = "";
    renderComponent(component, outlet);
  }

  route(ev) {
    if (ev.target.classList.contains("glimmer-link")) {
      ev.preventDefault();
      const url = new URL(ev.target.href);
      const [route] = this.registry.filter((r) => r.path === url.pathname);
      if (route) {
        history.pushState({}, "", url);
        this.renderPage(route.component);
      }
    }
  }

  handlePopState(event) {
    const [route] = this.registry.filter((r) => r.path === location.pathname);
    if (route) {
      this.renderPage(route.component);
    }
  }

  willDestroy() {
    document.removeEventListener("click", this.route);
    window.removeEventListener("popstate", this.handlePopState);
  }

  navigate(path) {
    const [route] = this.registry.filter((r) => r.path === path);
    if (route) {
      this.renderPage(route.component);
    }
  }

  start(ev) {
    const path = location.pathname || "/";
    this.navigate(path);
  }

  static template = hbs`
      {{yield}}
      <div id="glimmer-router-outlet"></div>
   `;
}

```

### Lazy loading components
One thing we need to notice here is that, we are eagerly loading all the component JS in the `App` component while
setting up the router. This will lead to performance issues when our component size becomes bigger and we have large number of components. 

Before code splitting, this is how our network request timeline look like in the Dev Tools Network panel

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/oy1slhu80rzgyljel2ds.png)

Other frameworks like React have come up with something like code-splitting JS bundles and lazy loading components. We can do something like this in our Glimmer apps with `dynamic imports` in the Router's `renderPage` function.

After code splitting, as you can see we will only load the respective components in the page instead of loading all the components code at one shot.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/fulpe5up188rmrqbl1vj.png)

```js
renderPage(component) {
    const outlet = document.getElementById("glimmer-router-outlet");
    outlet.innerHTML = "<h1>Loading page...</h1>";

    import(`./pages/${component}.js`).then(component => {
      outlet.innerHTML = "";
      renderComponent(component.default, outlet);
    });

  }
```


First we display the loading indicator, before fetching the component through dynamic imports, then once the promise is resolved, we will get the component instance and use the default import which is the component class definition and pass it to the `renderComponent` function.

For this to work, we need to change the prop type of the Route component by giving the component name instead of 
the component instance itself in `App.js`.

```html
<Router>
  <Route @path="/" @component="Home"/>
  <Route @path="/about" @component="About"/>
  <Route @path="/contact" @component="Contact"/>
</Router>
```

Please keep in mind that for code-splitting and lazy-loading to work properly we need to make some changes to our build configuration also. I am using [Snowpack](https://snowpack.dev) as my bundler and it automatically bundle the component source files separately. You can read more about bundling Glimmer apps with Snowpack in my previous blog post here.

{% link https://dev.to/rajasegar/building-glimmer-apps-with-snowpack-igc %}

If you are using [Webpack](https://webpack.js.org) you need to set up multiple entry-points in you `webpack.config.js`.

So this is how our full and final Router implementation will look like.

## GlimmerRouter.js
```js
import Component, { hbs, tracked } from "@glimmerx/component";
import { renderComponent } from "@glimmerx/core";

export class Link extends Component {
  static template = hbs`
  <a href={{@to}} class="glimmer-link">{{yield}}</a>
  `;
}

export class Route extends Component {
  constructor() {
    super(...arguments);
    const route = {
      path: this.args.path,
      component: this.args.component,
    };
    window.routerRegistry.push(route);
  }

  static template = hbs`{{yield}}`;
}

export class Router extends Component {
  @tracked registry = window.routerRegistry;

  constructor() {
    super(...arguments);
    window.routerRegistry = [];
    document.addEventListener("click", this.route.bind(this));
    window.addEventListener("popstate", this.handlePopState.bind(this));
    document.addEventListener("DOMContentLoaded", this.start.bind(this));
  }

  renderPage(component) {
    const outlet = document.getElementById("glimmer-router-outlet");
    outlet.innerHTML = "<h1>Loading page...</h1>";

    import(`./pages/${component}.js`).then(component => {
      outlet.innerHTML = "";
      renderComponent(component.default, outlet);
    });

  }

  route(ev) {
    if (ev.target.classList.contains("glimmer-link")) {
      ev.preventDefault();
      const url = new URL(ev.target.href);
      const [route] = this.registry.filter((r) => r.path === url.pathname);
      if (route) {
        history.pushState({}, "", url);
        this.renderPage(route.component);
      }
    }
  }

  handlePopState(event) {
    const [route] = this.registry.filter((r) => r.path === location.pathname);
    if (route) {
      this.renderPage(route.component);
    }
  }

  willDestroy() {
    document.removeEventListener("click", this.route);
    window.removeEventListener("popstate", this.handlePopState);
  }

  navigate(path) {
    const [route] = this.registry.filter((r) => r.path === path);
    if (route) {
      this.renderPage(route.component);
    }
  }

  start(ev) {
    const path = location.pathname || "/";
    this.navigate(path);
  }

  static template = hbs`
      {{yield}}
      <div id="glimmer-router-outlet"></div>
   `;
}

```

## References
- [A Modern Javascript Router in 100 Lines](https://krasimirtsonev.com/blog/article/A-modern-JavaScript-router-in-100-lines-history-api-pushState-hash-url)
- [Realword-example-app in Glimmer](https://github.com/dfreeman/glimmerx-realworld-example-app)

## Demo
The demo for this post is hosted [here](https://glimmer-7guis.surge.sh) and the source code for the same is available in this [repository](https://github.com/rajasegar/glimmer-7guis).

In the next post, we will replace all the boilerplate logic of our Router component with a more standard and established routing library which will take care of all the necessary things we missed in the Router like query parameters, dynamic segments, etc.,


Please let me know about any queries or feedback in the comments section. I will be happy to discuss more about the things in the post with you all.