Let’s code a client side router for your frameworkless SPA

Vijit Ail
Vijit Ail
May 19, 2019 · 7 min read

Yes, you read the title right, Client side router for a frameworkless SPA, that’s exactly what we are going to build in this story.

Before we dive into the code, let’s first understand what is client side routing.

A client side router runs on the user’s browser. The browser does not make a request to the server for the page, rather it looks into the JavaScript code that is loaded onto the browser to render the page for the matched route.

This is how our router would render the page —
1. Listen on hashchange event or custom event in case of history.pushState().
2. When the URL changes, match and parse the URL to the route predefined in the code.
3. Look for the view to be rendered for that route, if no match is found render a 404 message.

Image for post
Image for post

**Enough theory, show me the code already!**

Below is the folder structure that we’d be working with

Image for post
Image for post

Let’s define a class to represent the Route in the router folder.

Image for post
Image for post
router/Route.js

The Route class would take in 3 parameters while instantiating; name, path and the view associated to the route.

The setProps() method will set the props or properties that would be passed on to the route from the URL and the renderView() method would return the view of the route.

Image for post
Image for post
router/Router.js

The Router class will be the heart of our routing system. It will take an array of route objects as the first parameter and the renderNode where the view will be injected as the second.

Our Router class will have methods like match() and navigate() to match the routes and navigate respectively. Lets look at their definitions.

Image for post
Image for post
router/Router.js

The navigate() method will filter through each route and check if it matches with any of the predefined routes. If no route is matched the renderNode will render a 404 message, else it would render the matched route’s view;
In the match(), we are creating a regular expression of the route path and matching it with the requested path. If it matches we are also finding out if the requested route has any route parameters.

Well, you might have already noticed the problem in the navigation(), we are literally injecting the view using innerHTML, which btw must be avoided at all cost. Mainly because of security reasons regarding XSS and also another problem that I encountered while coding, that any element in the injected view does not have an event listener attached to it. So that means you cannot have a button in a view to navigate to another route, it simply won’t work. To get around this we need to build the view using document.createElement(). To keeps this post concise, I’m leaving it up to you to figure it out(I may explore it later).

Update: I

I like to add utility functionalities so let's define another method to add routes to the router.

router/Router.js

Let’s take a look at the index.js of router.

Image for post
Image for post
router/index.js

Here we are exporting a function which takes routes as the parameter and creates an instance of the Router Class. The renderNode is a div with an id of app. We are adding the click event listener to the buttons that have a route attribute (inspired from other libraries), and listening for the hashchange event on the url to call the navigate() method.

Now, lets take a look at all the views in the application (look at the captions if you get lost keeping tracks of files)

Image for post
Image for post
views/home.js
Image for post
Image for post
views/about.js
Image for post
Image for post
views/contact.js
Image for post
Image for post
views/profile.js

You can install html pragmas in your code editor for syntax highlighting the html in template literals.

And finally in our app.js file lets define all our routes and pass it to our router.

Image for post
Image for post
app.js

The following code would go in the body of the index.html file

Image for post
Image for post
index.html

Make sure to add the type=”module” in the script tag in order to use the import/export es6 feature, because of this we don’t have to setup any extra configuration.

To wrap this up lets add a few lines of CSS.

Image for post
Image for post
style.css

Update (2020)

This is more of an extension to the story than an edit. So, I explained how using innerHTML is a bad idea. Instead of rendering the template yourself, you can use other templating libraries like lit-html.

Image for post
Image for post
Updated profile view

Inside the profile view, I have imported a link() method which will redirect the user to another view. Earlier, with innerHTML is was not possible to attach events, but with lit-html you can attach any event using the event bindings which are prefixed with @ character.

There’s one more; to step to render the view you’ll have to use the render() method of lit-html.

So, inside the navigate() method of the Router class, replace the innerHTML line with the line below

render(route.renderView(), this.renderNode);

And at the top of the Router.js file import the render() method.

import { render } from "lit-html";

There’s one more thing I’d like to address is the hash in the URL. How do you remove the hash? If you don’t want the hash character to show up in the URL, you can use the History API’s pushState() method.

history.pushState({}, "", path);

So after the above updates, the navigate() method would like as follows.

Image for post
Image for post
Updated navigate() method

Now, let’s write the link() method, inside the router/index.js file.

Image for post
Image for post
link() method

The link() method, dispatches a custom event for push state because there’s no native event for the pushState() method.

And below instead of listening to hashchange event, you need to listen to the custom event that we just created.

Image for post
Image for post

That was all the updates. One thing you’ll notice is after the URL changes and you refresh the page, the browser sends a 404 error. There’s no way to handle this using JavaScript. You need to configure your apache or the node server to handle the routing to only serve the index page for any requested route.

I went on to set up a simple node server with express and also configured webpack to handle the modules.

Image for post
Image for post

I have updated the repo. You can find the link at the end of the story.

Conclusion

To conclude this post, I’d say that you don’t need to bring in other libraries for small and simple Single Page Application, you can spin up you own custom solution, I just happen to show you one of the many ways, probably you can do more and much better. If you do, do let me know, I’d be happy to see your work.

I referred to this video on YouTube for writing the story — https://www.youtube.com/watch?v=D1fLaNxd-ZM

You can take a look at the final repo in my github, if you get stuck while following along. You can DM me on instagram @vijit__ail if you come across any problem.

Cheers and happy coding 🍻😉

The Startup

Medium's largest active publication, followed by +719K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store