const - JS | lectureAdvanced DOM and Events

Event Delegation - Implementing Page Navigation

Advanced DOM and Events

In this lecture, we will use the power of event bubbling to implement something called event delegation. What we are going to do is to implement a smooth scrolling behaviour for our navigation links. Below is a video of what we are going to build.

Before we start, go ahead and dowload the starter files from this link. Once you have downloaded the starter files, open the index.html file in your browser. Right now if you click on any of the navigation links, you will see that the page will jump to the section you clicked on. But, of course, we want this to happen smoothly. So, let's go ahead and use event delegation to implement this navigation.

We will start by implementing it without using event delegation so that you can see the problem in the approach that we have been using so far. Hence, let's start by selecting our three navigation links and adding an event listener to each of them.

script.js
const navLinks = document.querySelectorAll('.nav__link');
navLinks.forEach(function (linkEl) {
  linkEl.addEventListener('click', function (e) {
    console.log('You clicked on a link');
  });
});

If you click on any of the links now, you should see the log message in the console. I guess now you already start to see the problem which is that by default, clicking on one of the links will scroll the page to the section that we want in the HTML document. And that's because of the so-called anchors (#section--1, #section--1 etc.). So, for example, if we click on the link which has an href attribute of #section--1, the browser will scroll to the section with the id of section--1. You can also see it in the URL bar. So, we need to prevent the default behaviour of the browser. And we can do that by calling the preventDefault method on the event object.

script.js
navLinks.forEach(function (linkEl) {
  linkEl.addEventListener('click', function (e) {
    e.preventDefault();    console.log('You clicked on a link');
  });
});

With the preventDefault method added, now when we click in the links, nothing happens. We no longer scroll to the section, and the URL bar no longer changes. Now that we have fixed the problem, let's go ahead and implement the smooth navigation.

The anchor we talked about earlier is still going to be very useful because, we can now take the the value of the href attribute and select the section we want to scroll to based on that value. Because, when we think about it, the anchor which is the value of the href attribute, looks pretty much like a selector for an id.

script.js
navLinks.forEach(function (linkEl) {
  linkEl.addEventListener('click', function (e) {
    e.preventDefault();
    const id = this.getAttribute('href');    console.log(id); // #section--1, #section--2, #section--3
  });
});

As I mentioned earlier, the result we get in the console looks pretty much like a selector already. Hence we can now take that value and use it to select the section we want to scroll to. And for that we are going to use the scrollIntoView method again.

script.js
navLinks.forEach(function (linkEl) {
  linkEl.addEventListener('click', function (e) {
    e.preventDefault();
    const id = this.getAttribute('href');
    document.querySelector(id).scrollIntoView({ behavior: 'smooth' });  });
});

Everything is now working as expected. But, the problem with this solution is that it is not efficient. It is not efficient because, when we take a closer look at it we notice that we are adding the same event handler once to each of the elements (links). And that's kind of unecessary. Of course, it would be fine for only three elements, but what if we had like a thousand elements? If we would attach an event handler to a thousand elements like we did above with the forEach method, then, we would effectively be creating a thousand copies of the same event handler function. And so, that will certainly impact the performance of our application. So, we need to find a better solution.

The best solution is to use event delegation. With event delegation, we use the fact that events bubble up and we do that by putting the event listener on a common parent element of all the elements we are interested in. In our case, we are interested in the navigation links. So, we can put the event listener on the nav__links (ul) element itself. Hence, when someone clicks on any of the links, the event is generated and bubbles up just as we saw in the previous lecture.

And then, we can basically catch that event in the parent element and then use the target property of the event object to determine which element actually triggered the event. So, that's what event delegation is all about. Let's go ahead and implement it.

In event delegation, we need two steps. First, we add the event listener to a common parent element of all the elements we are interested in. And then, we need to determine which element actually triggered the event. So, let's start by putting the event listener on the parent element.

script.js
document.querySelector('.nav__links').addEventListener('click', function (e) {
  // Do something
});

Now, we need to determine which element actually triggered the event. And we can do that by using the target property of the event object. So, let's log the target property to the console.

script.js
document.querySelector('.nav__links').addEventListener('click', function (e) {
  console.log(e.target);
});

Event delegation

One thing to note is that, you will notice that if you click in the middle .i.e, the specae between two links, you will si that you get the parent element (ul) in the console. And this part is actually very important to note because, now, we only want to work with the clicks that happended on one of the links. So now we need a matching strategy here in other to match only the elements that we are actually interested in. And the best way to do that is to check if the target has the class nav__link.

script.js
document.querySelector('.nav__links').addEventListener('click', function (e) {
  if (e.target.classList.contains('nav__link')) {    console.log('You clicked on a link');
  }
});

With this condition in place, we successfully only select the link elements that we are interested in. And now, we can go ahead and add the smooth scrolling behaviour we implemented earlier.

script.js
document.querySelector('.nav__links').addEventListener('click', function (e) {
  e.preventDefault();
  if (e.target.classList.contains('nav__link')) {
    /**
     * We now get the attribute on the element we clicked on
     * NOT on `this` which is the parent element
     */
    const id = e.target.getAttribute('href');
    document.querySelector(id).scrollIntoView({ behavior: 'smooth' });
  }
});

Everything still works beautifully, and we have successfully implemented the smooth navigation using event delegation which is a lot better, and a lot more efficient than simply attaching the same event handler to each of the elements.