Let’s code a client side router for your frameworkless SPA
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.
**Enough theory, show me the code already!**
Below is the folder structure that we’d be working with
Let’s define a class to represent the Route in the router folder.
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.
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.
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.
Let’s take a look at the index.js of router.
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)
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.
The following code would go in the body of the index.html file
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.
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.
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.
Now, let’s write the link() method, inside the router/index.js file.
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.
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.
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 🍻😉