Using Event Delegation for my JS SPA

Ross Enriques
6 min readFeb 1, 2022

As a coder if there’s one thing I always know is we want to make our code as DRY as possible. I am going to show you how I implemented event delegation on my tournament_formatter single page application for Flatiron.

My original HTML code I wrote

index.html
<button id='all'>All</button>
<button id='four'>4v4</button>
<button id='one'>1v1</button>
<button id='one-kid'>1v1kids</button>

This is my HTML it contained 4 buttons that I created where each button sorts the competition’s by name and value. This worked fine but each button being its own entity means adding an event listener for each element. As shown below

My original Javascript code I wrote

src/index.htmldocument.addEventListener('DOMContentLoaded', () => {
const fourVfour = document.querySelector('#four');
fourVfour.addEventListener('click', e => {
resetList()
Entry.sortByComp(1)
})
const allComps = document.querySelector('#all');
allComps.addEventListener('click', e => {
resetList()
Entry.allComps()
})
const oneOnone = document.querySelector('#one');
oneOnone.addEventListener('click', e => {
resetList()
Entry.sortByComp(2)
})
const oneOnoneKids = document.querySelector('#one-kid');
oneOnoneKids.addEventListener('click', e => {
resetList()
Entry.sortByComp(3)
})
})

As we can see this is not very DRY and very repetitive code I had all of these inserted in my DOMCONTENTLOADED as well which didn’t make sense to be in there because first off these are click events and second this is not good practice for separation of concerns. So although this may have all ran like I expected if I ever decided to add another button I would have to do multiple things to get it to work which is not what we want.

I also had created a class function for each button getting called on which was very repetitive code as well. I actually fixed this right away.

Original class functions

src/entry.jsstatic allComps()
let newEntry = () => { Entry.all.forEach(e => e.renderEntry()) }
return newEntry()
}
static oneAdult() {
let compEntry = this.all.filter(e => e.competition.id === 2)
let newEntry = () => { compEntry.forEach(e => e.renderEntry()) }
return newEntry()
}
static oneKids() {
let compEntry = this.all.filter(e => e.competition.id === 3)
let newEntry = () => { compEntry.forEach(e => e.renderEntry()) }
return newEntry()
}

New Class Function

src/entry.jsstatic sortByComp(compId) {
const compEntry = this.all.filter(e => e.competition.id === compId)
const newEntry = () => { compEntry.forEach(e => e.renderEntry()) }
return newEntry()
}

As you can see I was able to combine all functions into one by accepting a parameter which filters all Entries and stores it in a new array called compEntry and then I created another new variable called newEntry that iterates over the compEntry array and renders each entry again. Which I then return the newEntry() arrow function.

Now next is where event delegation comes into play instead of creating multiple addEventListeners for each element I will add only one. So to do this the first thing I needed to do were to have all my buttons in my HTML nested in a div tag. This is so I can access all elements by just accessing the div tag alone there would be no need to look for each button individually anymore.

My new HTML code I wrote

index.html<div id="comp-buttons">
<button id='all'>All</button>
<button id='four'>4v4</button>
<button id='one'>1v1</button>
<button id='one-kid'>1v1kids</button>
</div

So be wrapping them in this div I will then create a global variable to access this new div.

Global Variable containing node

src/index.jsconst compButtons = document.querySelector('#comp-buttons')

The next step was a little tricky I had to do deeper research on event delegation and I learned about the 3 phases for click events which is capture, target, and bubble. So I knew my next step is using capturing on the new parent div element that I had created. So what this meant is that I needed to add a click event listener on my compButtons which will capture the target button thats clicked.

Adding eventListener to parent

src/index.jscompButtons.addEventListener('click', e => {
const target = e.target
resetList()
})

What I have here is my parent div tag where all my buttons exist being the only element I need to add an eventListener too. I am also passing in the event using the callback function which contains what button is being clicked on. I then store it in a target class just to help simplify some code a bit. So by using e.target it pinpoints exactly what is being clicked on in this parent div and it will return a button element containing the child of compButtons. I also know I needed to add my function resetList() which does exactly what it says and renders an empty string.

The last thing I needed to do was create a conditional since there are 4 buttons that can be clicked on each button will have a different outcome.

New Javascript code using event delegation

compButtons.addEventListener('click', e => {const target = e.target
resetList()
if (target == compButtons.children[1]) {
Entry.sortByComp(1)
} else if (target == compButtons.children[2]) {
Entry.sortByComp(2)
} else if (target == compButtons.children[3]) {
Entry.sortByComp(3)
} else if (target == compButtons.children[0]) {
Entry.all.forEach(e => e.renderEntry())
}
})

So what’s going on here is depending on what the event target is being clicked I am using an if conditional that compares the target with each child of my parent div. Depending which child it compares to it will pass in the value of that competition in the sortByComp() class function. The only button that runs a little different is when we want to view all the entries again, Instead of calling the sortByComp() we are instead just going to iterate over all the Entries and call renderEntry() for each one. This will render all the entries again.

There was one last thing I needed to do to get this code running right which I got stuck on was because my script tags were loading in my head in my HTML file I needed to move it to my the bottom of my body tag. This is because we need our HTML code to load first before Javascript files are loaded.

Moving src files to bottom of body

index.html<body>...<script type="text/javascript" src="src/index.js"></script>
<script type="text/javascript" src="src/entry.js"></script>
</body>

Wanted to add an update on how I abstracted my code even more. As shown below:

index.html<div id=“comp-buttons”>
<button id=‘all’ class=‘all-button’>All</button>
<button id=‘four’ class=‘comp-buttons’ data-id=1>4v4</button>
<button id=‘one’ class=‘comp-buttons’ data-id=2>1v1</button>
<button id=‘one-kid’ class=‘comp-buttons’ data-id=3>1v1kids</button>
</div>

So what we see happening here is I’m adding class name attributes to my buttons that have data-id attributes as well. For the all button it won’t be calling on the sortByComp() function because it will render all entries instead.

src/index.jscompButtons.addEventListener('click', e => {resetList()if (e.target.classList.contains('comp-buttons'){    Entry.sortByComp(parseInt(e.target.dataset.id))
} else {
Entry.all.forEach(e => e.renderEntry())
}
// if (target == compButtons.children[1]) {
// Entry.sortByComp(1)
// } else if (target == compButtons.children[2]) {
// Entry.sortByComp(2)
// } else if (target == compButtons.children[3]) {
// Entry.sortByComp(3)
// } else if (target == compButtons.children[0]) {
// Entry.all.forEach(e => e.renderEntry())
// }
})

So as we see above we can see the abstraction I did here if we compare it the hard coded commented out conditional I have at the bottom. By doing this I don’t need to add any code here anymore if I were to ever add another buttons that sorts another competition. What my new conditional is doing is it’s checking on the event target that’s getting clicked on and checking it’s class attributes to see if it has the value of comp-buttons existing. If that returns true it will then call on the class function sortByComp() and pass in the target.dataset-id as the argument which is acquired because I added a data-id in the button’s HTML. Then I needed to parseInt() because it was coming back as a string. And the last part of this code is the else statement since I included an all-button that renders all entries I made it have it’s own class called all-button which I could've left blank as well but named it to keep it more descriptive. So if the event target doesn’t have the class attribute called comp-buttons then it renders all entries instead.

--

--