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>
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:
- The state that will be pushed along the URL, which can be a light JS object or some kind of structured data
- An unused parameter, mandatory due to backward compatibility, that we can replace by an empty string
- 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>
And test it:
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>
And we now have our virtual page!
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>
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>
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>
And we are done!
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>
Top comments (5)
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.
I didn't know you can do in this way too. Thank you👍🔥
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.
Thanks!
Indeed it could be, I kept this as accessible as I can but such a pattern could help making this approach more efficient
Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 🫰