DEV Community

loading...

A Router Without a Web Server in Vanilla JavaScript

aminnairi profile image Amin Updated on ・5 min read

I saw a post about how to create a router in pure Vanilla JavaScript. Since it was not talking about hashed routing, I decided to create this post to share my knowledges with you.

Why would I do that?

Building a router in JavaScript has now become trivial thanks to the History API. It is largely supported and let you build your router the way you want, without relying on a third-party library. In Vue.js, you can even build your own homemade router and plug it thanks to Vue.js Plugins. In fact, the official router library for Vue.js, Vue Router, relies on the History API. But not only. Because it optionally let you build the so called Hashed Router.

What is a Hashed Router?

It is a router that does not rely on the History API, but rather on the hash URL of your website. Let's say you have a title on your web page.

<h2>About me</h2>
<p>Lorem ipsum dolor sit amet...</p>

And you wanted your users to jump directly to this section whenever they are in the head of your page. You would want to use an id attribute for your title.

<h2 id="about">About me</h2>

And create a link, like in your header, to redirect your users to this section.

<header>
  <a href="#about">About me</a>
</header>

Now, if you click on your link, the URL should go from

http://yoursite.com

To

http://yoursite.com#about

Nothing fancy, right?

Why would I want to use a Hashed Router?

The thing with a History API based router is that it relies on the origin of the page to work. If you try to open your HTML page where you implemented your History API, you should get something like this:

Failed to execute 'pushState' on 'History': A history state object with URL 'file:///path/to/index.html' cannot be created in a document with origin 'null'

This is because, by default, HTML documents opened as files have an origin set to null. This is a problem.

Instead, hased-based routers do not rely on that but rather on an event fired by the window object. This event will be fired when we change the hash of the url. In our previous example, clicking on the link would fire this event up. No web server needed.

How can I implement a Hashed router?

It is as simple as using only one event. the onHashChange event.

<!DOCTYPE html>
<html>
  <body>
    <a href="#home">Home</a>
    <a href="#about">About</a>
    <script src="script.js"></script>
  </body>
</html>
function onRouteChanged() {
  console.log("Hash changed!");
}

window.addEventListener("hashchange", onRouteChanged);
Hash changed!
Hash changed!

Try it online.

Implementing the routing

We now need to fetch the route that has been issued by the user. We can use the window.location.hash property to get the value of the current "route".

  function onRouteChanged() {
-   console.log("Hash");
+   console.log(window.location.hash);
  }
#home
#about

Try it online.

We have everything we need now. We can start implementing a view renderer for our router.

      <a href="#about">About</a>
+     <a href="#contact">Contact</a>
+     <main id="router-view"></main>
      <script src="script.js"></script>

I also added another link. This will help me show you how we can also implement a 404 - page not found handler. You'll be amazed how easy it is.

Next, we need to add a little bit more logic to our onRouteChange handler so that it can render our routes the way a router would.

  function onRouteChanged() {
-   console.log(window.location.hash);
+   const hash = window.location.hash;
+   const routerView = document.getElementById("router-view");
+ 
+   if (!(routerView instanceof HTMLElement)) {
+     throw new ReferenceError("No router view element available for rendering");
+   }
+ 
+   switch (hash) {
+     case "#home":
+       routerView.innerHTML = "<h1>Home page</h1>";
+       break;
+ 
+     case "#about":
+       routerView.innerHTML = "<h1>About page</h1>";
+       break;
+ 
+     default:
+       routerView.innerHTML = "<h1>404 - Page Not Found</h1>";
+       break;
+   }
  }

Try it online.

I stored the hash URL in a variable so that I can use the switch statement to render a different HTML content depending on the route that has been issued by the user. I also stored the router view element to check if the element is indeed in the document (we never know what can happen and it will be nice to have some eloquent error message in this case). I also need it to update the innerHTML content of the router in the switch statement.

The default statement of the switch will be triggered with our contact link because we did not specify any handler for it in our switch.

That's it! You have a very basic router, working everywhere, whether it is hosted on a web server, or shared as a single HTML page. I can see some use-cases when you need to show a quick prototype of a website to a client for instance. All he has to do is open the page in his browser and tada!

Limitations

Of course, there is an obvious limitation to this kind of routing because we are using the hash of the URL, and hacked its original purpose to use it as a router. But if we need to use regular hrefs in our page, it would simply break the routing as it will trigger our hash changed handler.

Solution

A solution I found for this problem, probably not the best but it is worth it if you absolutely need to use hash-based routing, is to use a data-* attribute along with a little of JavaScript.

<button data-go-to-id="a-little-introduction">to the intro</button>
<!-- later in the document -->
<h2 id="a-little-introduction>A little introduction</h2>
"use strict";

document.querySelectorAll("[data-go-to-id]").forEach(function(link) {
  link.addEventListener("click", function() {
    const element = document.getElementById(link.dataset.goToId);

    if (!(element instanceof HTMLElement)) {
      throw new ReferenceError(`Unable to found element with id "${goToId}"`);
    }

    window.scroll({
      top: element.getBoundingClientRect().top,
      left: 0,
      behavior: "smooth"
    });
  });
});

The smooth scrolling on some device wont work (I think of some Apple devices in particular) but this would be one of the, I'm sure, many solutions you could find to solve this problem. My solution has the disavantage that it cannot be used in shared links like Hey, look what I found here: http://yoursite.com#home#title-of-article. I let this as an exercise for the reader to implement a better solution.

Conclusion

Hash-based routers are another way of routing your users without having to reload the page. This is also handy when creating GitHub Pages as we do not have to rethink our History-based router and prefix all our routes with the subdirectories like /github-repo/about.

If you do not need to use a lot of href redirections and don't want/can't use the History API, then this can be a good solution to have a router in your page.

What I showed you is a very basic implementation of a hased-based router. If you want to go further, you could:

  • Implement this router inside an object, like new HashedRouter to make the API more easy to use. Espacially with methods like addRoute and start.
  • Find a better solution than what I used to implement links in the page.

Discussion (6)

pic
Editor guide
Collapse
michdo93 profile image
Michdo93

Maybe a better solution is to use jQuery. I don`t have rewritten your code completly in jQuery. The only thing I changed is that I import .html template files with the jQuery function .load().

repl.it/@Michdo93/HummingParallelP...

I only want to show, that with easy changes you could create a much more useable solution. If you want to add as example a MVC pattern jQuery could also be a better choice. And if you want to fetch Data maybe over Ajax also jQuery make things much easier. And you have a simple solution, which is what modern frameworks do. MVC, Observer pattern and routing. You can do it only using pure JS or jQuery. The last one is an much easier way.

Collapse
aminnairi profile image
Amin Author

Hi there and thanks for contributing to this article!

That looks awesome rewritten in jQuery! I'm pretty sure people will find it very interesting. And for people that don't/can't use a framework, this example still remains relevant.

I would also like to add that you can go even further by using jQuery everywhere in this slightly updated example.

function onRouteChanged() {
  const hash = window.location.hash;
  const routerView = $("#router-view"); // updated

  if (!(routerView instanceof HTMLElement)) {
    throw new ReferenceError("No router view element available for rendering");
  }

  switch (hash) {
    case "#home":
      //routerView.innerHTML = "<h1>Home page</h1>";
      routerView.load('views/home.html'); // updated
      break;

    case "#about":
      //routerView.innerHTML = "<h1>About page</h1>";
      routerView.load('views/about.html'); // updated
      break;

    default:
      //routerView.innerHTML = "<h1>404 - Page Not Found</h1>";
      routerView.load('views/404.html'); // updated
      break;
  }
}

$(window).on("hashchange", onRouteChanged); // updated
Enter fullscreen mode Exit fullscreen mode
Collapse
tangopj profile image
Thread Thread
aminnairi profile image
Amin Author

That's really clever, well done!

Except styles don't apply to the <object> tag sadly and I think it would be very inconvenient to style elements using this technique and an external CSS file.

But if you are designing a markup-only web page this is the way to go (or use inline styles if you are brave enough).

But this is a great tag to render some multimedia content like videos or PDF files!

Thread Thread
tangopj profile image
TangoPJ

Yep, I have a problem with styling my content, I think how can I do that now. Thank you for your return

Collapse
lepinekong profile image
lepinekong

interesting hack thanks.