DEV Community

Cover image for Virtual URL navigation using vanilla JavaScript
Pierre Bouillon
Pierre Bouillon

Posted on

Virtual URL navigation using vanilla JavaScript

I came upon an issue on a GitHub project where the maintainer wanted to dynamically change the content of the page and give the feeling of it being a "native" page, without actually creating another HTML file.

He also wanted his website to only use vanilla JS and HTML files so introducing Angular, Svelte or any other framework to him was not an option.

Fortunately, I discovered an interesting API introduced in HTML5 that can solve exactly this kind of issue.

Let's explore it step by step.

Setup

For our example, we will build a very simple page that can give the feeling of using some kind of routing.

Firstly, we will create our simple HTML page:



<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Fake URL navigation</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <h1>Fake routing</h1>
    <button id="details">Show details</button>
  </body>
</html>


Enter fullscreen mode Exit fullscreen mode

Which does not renders much for now.

Changing the URL

We now want to change the URL whenever the user clicks on the button.

To do so, we can use the pushState function to programmatically add a new entry to the history, which will also result in the URL being modified.

Its usage is fairly simple and made of three parameters:

  1. The state that will be pushed along the URL, which can be a light JS object or some kind of structured data
  2. An unused parameter, mandatory due to backward compatibility, that we can replace by an empty string
  3. The actual URL we want to push

Let's add a small script to change this:



<script>
  document.getElementById("details").onclick = (_event) => history.pushState({}, "", "/details");
</script>


Enter fullscreen mode Exit fullscreen mode

And test it:

URL update

Wonderful! We may now focus on the content

Updating the page

In order to update our content, we will need some kind of template and to substitute the current HTML body by it.

Using the native API, this is pretty straightforward:



<script>
+ const template = "<h1>Details</h1> <p>Some highly useful information</p>";

  document.getElementById("details").onclick = (_event) => {
    history.pushState({}, "", "/details");
+   document.body.innerHTML = template;
  };
</script>


Enter fullscreen mode Exit fullscreen mode

And we now have our virtual page!

Replace content

Fixing the back button

With our current solution, once the details page has been shown, it is not possible to display the index again without having to reload the whole page.

To fix this, we can start by creating a local history by pushing the content of the body element into an stack before removing it:



<script>
  const template = "<h1>Details</h1> <p>Some highly useful information</p>";

+ const bodyHistory = [];

  document.getElementById("details").onclick = (_event) => {
    history.pushState({}, "", "/details");

+   bodyHistory.push(document.body.innerHTML);
    document.body.innerHTML = template;
  };
</script>


Enter fullscreen mode Exit fullscreen mode

Then, we can use the event that is kind of the mirror of pushState: popstate.

By listening to it, we will know whenever the back button has been pressed and we will be able to restore the previous content of the page:



<script>
  const template = "<h1>Details</h1> <p>Some highly useful information</p>";

  const bodyHistory = [];

  document.getElementById("details").onclick = (_event) => {
    history.pushState({}, "", "/details");

    bodyHistory.push(document.body.innerHTML);
    document.body.innerHTML = template;
  };

+ onpopstate = (_event) => {
+   const previousContent = bodyHistory.pop();
+
+   if (previousContent) {
+     document.body.innerHTML = previousContent;
+   }
+ };
</script>


Enter fullscreen mode Exit fullscreen mode

At this point going back in the history would work but we won't be able to navigate again because we restored the whole page but the event listener would not have been set again.

A quick way to fix it would be to re-register it inside our onpopstate event handler in the same way we did when we first add this logic.

Let's extract this logic into a dedicated function and call it:



<script>
  const template = "<h1>Details</h1> <p>Some highly useful information</p>";

  const bodyHistory = [];

+ registerHandler();

+ function registerHandler() {
+   document.getElementById("details").onclick = (_event) => {
+     history.pushState({}, "", "/details");
+
+     bodyHistory.push(document.body.innerHTML);
+     document.body.innerHTML = template;
+   };
+ };

  onpopstate = (_event) => {
    const previousContent = bodyHistory.pop();

    if (previousContent) {
      document.body.innerHTML = previousContent;
+     registerHandler();
    }
  };
</script>


Enter fullscreen mode Exit fullscreen mode

And we are done!

With back


In this small article we saw how to take advantage of the couple pushState/popState to give the user the feeling of browsing a different page.

This is a light introduction to some kind of virtual navigation but, in my case, it helped me to successfully implement it into this maintainer's project.

However, please note that reloading the page when the user is on our URL that does not match an actual route will result in an HTTP 404 error.

One solution would be to replace the navigation to our virtual pages by hashes (like ...#details instead of .../details). This way, the browser would still render our root page on reload and we would be able to intercept whatever is after the hash symbol to render our details page instead.

I hope that you learned something useful there!


Entire sources used for the demo:



<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Fake URL navigation</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <h1>Fake routing</h1>
    <button id="details">Show details</button>
  </body>

  <script>
    const template = "<h1>Details</h1> <p>Some highly useful information</p>";

    const bodyHistory = [];

    registerHandler();

    function registerHandler() {
      document.getElementById("details").onclick = (_event) => {
        history.pushState({}, "", "/details");

        bodyHistory.push(document.body.innerHTML);
        document.body.innerHTML = template;
      };
    }

    onpopstate = (_event) => {
      const previousContent = bodyHistory.pop();

      if (previousContent) {
        document.body.innerHTML = previousContent;
        registerHandler();
      }
    };
  </script>
</html>


Enter fullscreen mode Exit fullscreen mode

Top comments (5)

Collapse
 
turowski profile image
Kacper Turowski • Edited

However, please note that reloading the page when the user is on our URL that does not match an actual route will result in an HTTP 404 error.

Worse even, the subpages are unlinkable to others. Funnily enough, I recently wrote an article on devto about using hash-routing instead of faking url routing, like you mentioned being possible. This provides you not only with ability to reload pages but also link to the subpages. I was surprised how easy it is to implement a full-blown router on the client side.

Collapse
 
varshithvhegde profile image
Varshith V Hegde

I didn't know you can do in this way too. Thank you👍🔥

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Nice article. Thank you very much. It seems that this can be improved by using the Memento pattern to implement the back and forth going between the pages.

Collapse
 
pbouillon profile image
Pierre Bouillon

Thanks!

Indeed it could be, I kept this as accessible as I can but such a pattern could help making this approach more efficient

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 🫰