Anchor links pointing to a fragment fires History API's "popstate" event
I hit an interesting "bug" while working on a SPA application. Clicking an anchor link that points to a fragment on the page fires the History API's popstate
event. This triggers some logic on my end, which was not my intent. I fixed that with a bit of JavaScript, and I had to share the snippet.
The context
So, let's say that we have a page with the following HTML:
<div id="container">
<a href="#foobar">click me</a>
<p>...long text here...</p>
<h1 id="foobar">A title here</h1>
<p>...long text here...</p>
</div>
And the following JavaScript:
window.addEventListener('popstate', () => {
document.querySelector('#container').innerHTML = 'Oh snap!'
});
If I clicked the link, I thought the browser would scroll the page to the <h1>
element. However, because I had this popstate
listener, the browser reloaded the content of my main container. And that's the correct behavior from a browser's point of view. However, I wanted something different, so I "fixed" it using JavaScript.
The "fix"
I had to get all the links that point to fragments and prevent their default behavior. So, I started with the following:
const links = Array.prototype.slice.call(document.querySelectorAll('a'), 0);
links.filter(el => (el.getAttribute('href') || '').match(/^#/)).forEach(el => {
// ...
});
The next thing was to attach a click
listener and call preventDefault()
on the event. Then make the browser scroll to the correct DOM element (if that element exists).
const links = Array.prototype.slice.call(document.querySelectorAll('a'), 0);
links.filter(el => (el.getAttribute('href') || '').match(/^#/)).forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
const toTarget = document.querySelector(el.getAttribute('href'));
if (toTarget) {
if (toTarget.scrollIntoView) {
toTarget.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
} else {
let rect = toTarget.getBoundingClientRect();
window.scrollTo(rect.x, rect.y)
}
}
})
});
I had to write a bit of a fallback in case scrollIntoView
is not supported.