Despite the fact that in the end I completely used CSS for
this project , it all started with the use of JavaScript and classes.
However, I had a problem. I wanted to use the so-called Pop-Up Events, but also I wanted to minimize the dependencies that I would have to implement. I didn’t want to include jQuery libraries for “this little test”, just to use pop-up events.
Let's take a closer look at what pop-up events are, how they work, and consider several ways to implement them.
Okay, so what's the problem?
Consider a simple example:
')
Suppose there is a list of buttons. Every time I click on one of them, it should become "active." After pressing the button again, it should return to its original state.
Let's start with HTML:
<ul class="toolbar"> <li><button class="btn">Pencil</button></li> <li><button class="btn">Pen</button></li> <li><button class="btn">Eraser</button></li> </ul>
I could use a standard JavaScript event handler like this:
for(var i = 0; i < buttons.length; i++) { var button = buttons[i]; button.addEventListener("click", function() { if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }); }
It looks good ... But it will not work. At least not the way we expect it.
Closures won
For those who know a little functional JavaScript, the problem is obvious.
For the rest I will briefly explain - the handler function is closed to the variable
button . However, this is a single variable, and overwrites each iteration.
In the first iteration, the variable refers to the first button. In the next - on the second, and so on. But, when the user presses a button, the cycle has already ended, and the
button variable refers to the last button, which always causes an event handler for it. Disorder.
What we need is a separate context for each function:
var buttons = document.querySelectorAll(".toolbar button"); var createToolbarButtonHandler = function(button) { return function() { if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }; }; for(var i = 0; i < buttons.length; i++) { buttons[i].addEventListener("click", createToolBarButtonHandler(buttons[i])); }
Much better! And most importantly, it works correctly. We have created the
createToolbarButtonHandle function that returns an event handler. Then for each button we hang our handler.
So what's the problem?
And it looks good and it works. Despite this, we can still make our code better.
First, we create too many handlers. For each button inside the
.toolbar we create a function and bind it as an event handler. For three buttons, memory usage is negligible.
But if we have something like this:
<ul class="toolbar"> <li><button id="button_0001">Foo</button></li> <li><button id="button_0002">Bar</button></li>
then the computer, of course, will not explode from overflow. However, our memory usage is far from perfect. We allocate a huge amount of it, although you can do without it. Let's rewrite our code again so that we can use one function several times.
Instead of referring to the
button variable to keep track of which button we clicked, we can use the
event object (the “event” object), which is passed as the first argument to each event handler.
An event object contains some event data. In our case, we are interested in the
currentTarget field. From it, we get a link to the item that was clicked:
var toolbarButtonHandler = function(e) { var button = e.currentTarget; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }; for(var i = 0; i < buttons.length; i++) { button.addEventListener("click", toolbarButtonHandler); }
Fine! We not only simplified everything to a single function that is used several times, we also made our code more readable by removing the extra generator function.
However, we still can do better.
Suppose we added a few buttons to the sheet already after our code was executed. Then we would also need to add event handlers for each of them. And we would have to keep a link to this handler and links from other places. It does not look too tempting.
Perhaps there is another approach?
To begin with, let's figure out how events work and how they move along our DOM.
How do most of them work?
When a user clicks on an item, an event is generated to notify the application of this. Each event travels in three stages:
- Interception phase
- Event occurs on target.
- Ascent phase
Note: not all events go through a stage of interception or ascent, some are created immediately on the element. However, this is rather an exception to the rule.
The event is created outside the document and then sequentially moves through the DOM hierarchy to the
target (target) element. Once it has reached its goal, the event is selected in the same way from the DOM element.
Here is our HTML template:
<html> <body> <ul> <li id="li_1"><button id="button_1">Button A</button></li> <li id="li_2"><button id="button_2">Button B</button></li> <li id="li_3"><button id="button_3">Button C</button></li> </ul> </body> </html>
When the user presses button A, the event travels as follows:
Start
| #document
|
Interception phase| HTML
| BODY
| UL
| LI # li_1
| Button A
<- Event occurs on target|
Ascent phase| LI # li_1
| UL
| BODY
| HTML
v #document
Notice that we can trace the way in which an event moves to its target element. In our case, for each pressed button, we can be sure that the event will pop up back, passing through its parent -
ul element. We can use this and implement pop-up events.
Popup events
Pop-up events are those events that are tied to the parent element, but are executed only if they satisfy a condition.
As a concrete example, take our toolbar:
ul class="toolbar"> <li><button class="btn">Pencil</button></li> <li><button class="btn">Pen</button></li> <li><button class="btn">Eraser</button></li> </ul>
Now, knowing that any button click on the button will pop up through the
ul.toolbar element, let's attach our event handler to it. Fortunately, we already have it:
var toolbar = document.querySelector(".toolbar"); toolbar.addEventListener("click", function(e) { var button = e.target; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); });
Now we have a much cleaner code, and we even got rid of loops! Notice, however, that we replaced
e.currentTarget with
e.target . The reason is that we handle events at a different level.
e.target is the actual purpose of the event, where it is sneaking through the DOM, and from where it will emerge.
e.currentTarget - the current item that handles the event. In our case, this is
ul.toolbar .
Improved pop-up events
At the moment we are processing any click on each element that pops up through
ul.toolbar , but our verification condition is too simple. What would happen if they had a more complex DOM, including icons and elements that were not created to be clicked?
<ul class="toolbar"> <li><button class="btn"><i class="fa fa-pencil"></i> Pencil</button></li> <li><button class="btn"><i class="fa fa-paint-brush"></i> Pen</button></li> <li class="separator"></li> <li><button class="btn"><i class="fa fa-eraser"></i> Eraser</button></li> </ul>
Oops! Now, when we click on
li.separator or an icon, we add the
.active class to
it . At least it's not good. We need a way to filter events so that we react to the element we need.
Create a small helper function for this:
var delegate = function(criteria, listener) { return function(e) { var el = e.target; do { if (!criteria(el)) continue; e.delegateTarget = el; listener.apply(this, arguments); return; } while( (el = el.parentNode) ); }; };
Our assistant does two things. First, it bypasses each element and its parents and checks whether they satisfy the condition passed in the
criteria parameter. If an element satisfies, the assistant adds a field to the event object, called
delegateTarget , in which the element that satisfies our conditions is stored. And then it calls the handler. Accordingly, if no element satisfies the condition, no handler will be called.
We can use it like this:
var toolbar = document.querySelector(".toolbar"); var buttonsFilter = function(elem) { return elem.classList && elem.classList.contains("btn"); }; var buttonHandler = function(e) { var button = e.delegateTarget; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }; toolbar.addEventListener("click", delegate(buttonsFilter, buttonHandler));
What the doctor prescribed: one event handler attached to one element that does all the work. But it does it only for the elements we need. And it responds well to adding and removing objects from the DOM.
Summing up
We briefly reviewed the basics of implementing the delegation (pop-up handling) of events in pure JavaScript. This is good because we don’t need to generate and attach a bunch of handlers for each item.
If I wanted to make a library out of it or use code in development, I would add a couple of things:
Assistant function to check the satisfaction of the object criteria in a more unified and functional form. Like:
var criteria = { isElement: function(e) { return e instanceof HTMLElement; }, hasClass: function(cls) { return function(e) { return criteria.isElement(e) && e.classList.contains(cls); } }
Partial use of an assistant would also be superfluous:
var partialDelgate = function(criteria) { return function(handler) { return delgate(criteria, handler); } };
Original article:
Understanding Delegated JavaScript Events(From the translator: my first, judge strictly.)
Happy coding!