My 5th book "50 Tips on JavaScript" is on sale. Go get it from https://50tips.dev.
Illustrated guide to 50 of the most interesting JavaScript topics.

Anchor links pointing to a fragment fires History API's "popstate" event

Anchor links pointing to a fragment fires History API's Photo by CHUTTERSNAP

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.

If you enjoy this post, share it on Twitter, Facebook or LinkedIn. To leave a comment go here.