Javascript page transition issue

I’m using Javascript to create some page transition effect to avoid reloading pages while navigating on a website.

The interface of the website is divided in 2 parts. The first part is the menu that is present in all pages of the site. The second part is the main content that refreshs by loading the new content of the page.

The issue is, when I have a link inside the main content main, the link doesn’t trigger the page transition. It only works with the links inside the menu.

const main = document.querySelector('.js-content');
const links = [...document.querySelectorAll('a')];

let isAnimating = false;

links.forEach(link => {
    link.addEventListener('click', async e => {
        e.preventDefault();
        if(isAnimating) return
        const url = e.target.href;
        console.log(url)
        startTransition(url);
        const pathname = new URL(url).pathname;
        history.pushState(null, '', pathname);
    })
})

window.addEventListener('popstate', e => {
    const url = window.location.pathname;
    startTransition(url)
})

const startTransition = async (url) => {
    isAnimating = true;
    const html = await fetch(url);
    const htmlString = await html.text();
    const parser = new DOMParser();
    const parsedhtml = parser.parseFromString(htmlString, 'text/html').querySelector('.js-content')

    transitionDiv.classList.add('is-animate-in');
    transitionDiv.addEventListener('transitionend', () => {
        main.innerHTML = parsedhtml.innerHTML;
        transitionDiv.classList.remove('is-animate-in');
        transitionDiv.classList.add('is-animate-out');
        setTimeout(() => {
            transitionDiv.style.transition = '0s';
            transitionDiv.classList.remove('is-animate-out');

            setTimeout(() => {
                transitionDiv.style.transition = '1s';
            }, 100)
            isAnimating = false;
        }, 1000)
    }, {once: true})
}

I read some topics about JS Event Delegation. I tried targeting the node a in the main content, but it doesn’t seem to work.

main.addEventListener("click", async (e) =>  {
    if(e.target && e.target.nodeName == "a") {
        e.preventDefault();
        if(isAnimating) return
        const url = e.target.href;
        console.log(url)
        startTransition(url);
        const pathname = new URL(url).pathname;
        history.pushState(null, '', pathname);
    }
});

You can reproduce the issue one this Sandbox.

Thank you.

Well remember that if you change part the DOM you are going to have to rebind your event listeners in that part of the DOM. So right now you are adding a click event to all links when you first load the page, but then when you click the link in the body, you are changing that part of the page, but never rebinding the click event to those new links.

So what you want to do that after the page has transitioned, look to bind a new event listener for click events to the body again. (Be mindful however that you don’t add several events over and over again or you can cause issues).

I hope this pushes you in the right direction.

1 Like

Your delegation is almost correct, but “a” should be uppercase “A”.

if (e.target && e.target.nodeName === "A") {
...
}
1 Like

Thanks @James_Hibbard, you’re right! It works with the uppercase A. I was wondering, would it possible to target the node a but with also this selector parameter a:not([data-no-transition])?

Thank you @Martyr2 for showing me the right direction. The solution is to add the uppercase inside the event delegation as mentioned by @James_Hibbard.

It’s better to use:

e.target.closest('a')

instead of direct e.target, in case a child element inside <a> is clicked.

Try this:

const link = e.target.closest('a:not([data-no-transition])');
3 Likes

I know you have moved on to event delegation, but just for future reference a nodelist has the forEach method on it’s prototype, so there is no need to convert it to an array first.


This would work

document.querySelectorAll('.link').forEach((link) => {
    link.addEventListener('click', () => {...})
}

As it has a Symbol.iterator you can also use a for loop e.g.

for (const link of document.querySelectorAll('.link')) {
    link.addEventListener('click', () => {...})
}
1 Like

Thank you @James_Hibard, this is exactly the method I was looking for. I didn’t know about .closest. For example in my webdev project, I have images img include inside the tag a, so the click event didn’t work.

However, I’m not sure to understand where would you use the code you provided inside the click event:

const link = e.target.closest('a:not([data-no-transition])');

1 Like

You place it at the start of your main.addEventListener("click", ...) block like so:

main.addEventListener("click", async (e) => {
  const link = e.target.closest('a:not([data-no-transition])');
  if (!link) return; // If no valid <a>, stop here

  e.preventDefault();
  if (isAnimating) return;
  const url = link.href;
  console.log(url);
  startTransition(url);
  const pathname = new URL(url).pathname;
  history.pushState(null, '', pathname);
});
1 Like

I almost got it. I was doing if (link) and not if (!link).

Thanks a lot for all your help :slight_smile:

1 Like