Javascript event delegation, handling parents of clicked elements? - javascript

http://jsfiddle.net/walkerneo/QqkkA/
I've seen many questions here either asking about or being answered with event delegation in javascript, but I've yet to see, however, how to use event delegation for elements that aren't going to be the targets of the click event.
For example:
HTML:
<ul>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
</ul>​
CSS:
ul{
padding:20px;
}
li{
margin-left:30px;
margin-bottom:10px;
border:1px solid black;
}
.d{
padding:10px;
background:gray;
}
​
What if I want to add a click event to handle the li elements when they're clicked? If I attach an event handler to the ul element, the divs will always be the target elements. Apart from checking every parent of the target element in a click function, how can I accomplish this?
edit:
I want to use event delegation instead of:
var lis = document.getElementsByTagName('li');
for(var i=0;i<lis.length;i++){
lis[i].onclick = function(){};
}
But if I do:
document.getElementsByTagName('ul')[0].addEventListener('click',function(e){
// e.target is going to be the div, not the li
if(e.target.tagName=='LI'){
}
},false);
EDIT: I'm not interested in how to use Javascript libraries for this, I'm interested in how they do it and how it can be done with pure js.

Here's one way to solve it:
var list = document.getElementsByTagName('ul')[0]
list.addEventListener('click', function(e){
var el = e.target
// walk up the tree until we find a LI item
while (el && el.tagName !== 'LI') {
el = el.parentNode
}
console.log('item clicked', el)
}, false)
This is overly simplified, the loop will continue up the tree even past the UL element. See the implementation in rye/events for a more complete example.
The Element.matches, Node.contains and Node.compareDocumentPosition methods can help you implement this type of features.

There is now a method on elements called closest, which does exactly this. It takes a CSS selector as parameter and finds the closest matching ancestor, which can be the element itself. All current versions of desktop browsers support it, but in general it is not ready for production use. The MDN page linked above contains a polyfill.

Related

Is there a way to call eventListner on any of multiple elements with same className or ID. Only the first child got called in my code

devs.
I got stuck while building a project.
I have a list of menu items, about ten of them, with the same ID name and I will like to edit each of them if any
is clicked.
using js, I did this;
const menuElement = document.querySelector('#menuElement')
const darktheme = document.querySelector('.dark')
loadEventListener()
function loadEventListener() {
menuElement.addEventListener('click', draw)
}
function draw() {
menuElement.style.background = 'var(--primary-color)'
}
However, when I click on the first menu element, it responds. but, it does not for the rest.
Thanks.
Yes. But, instead of setting up multiple event listeners on all the elements you want, use "Event Delegation" and set up a single event listener on a common ancestor element, then let the event bubble up to that ancestor. In the event callback, you then check to see if the event originated at an element that you care to handle and, if so, handle it.
And note, that ids must be unique, however multiple elements can have the same class, so that's the way to go.
EXAMPLE:
// Set up an event handler on a common ancestor
document.addEventListener("click", function(event){
// Check to see if the event originated at an element with the "foo" class
if(event.target.classList.contains("foo")){
// Handle the event any way you want
event.target.classList.add("bold");
}
});
.bold { font-weight:bold; }
<p>Clilck on any of the lines. Only the ones with the "foo" class will get bolded.</p>
<div class="foo">XYZ</div>
<div class="fooBar">XYZ</div>
<div class="foo">XYZ</div>
<div class="fooBar">XYZ</div>
<div class="foo">XYZ</div>
<div class="fooBar">XYZ</div>
You can use querySelectorAll() to use a CSS selector and get all elements in the document that match that selector in a NodeList. Then you could use forEach() to attach an event listener to each of those elements.
Here a small working example.
window.addEventListener("DOMContentLoaded", e => {
document.querySelectorAll(".myclass").forEach((element, i) => {
element.addEventListener("click", () => console.log(`Div ${i} was clicked.`))
});
})
.myclass {
border: 1px solid black;
padding: 10px;
margin: 5px;
}
<div class="myclass">Div 0</div>
<div class="myclass">Div 1</div>
<div class="myclass">Div 2</div>
You should also wait for the DOM to be loaded before attaching event handlers using the DOMContentLoaded event.
You cannot use an ID as IDs must be unique!
Another way to do it is using event delegation as outlined in Scott Marcus's answer.

jQuery keydown callback listens only on outer <ul> rather than inner <li> element

Hey so here is the code demo I made
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
</head>
<body>
<ul contenteditable class="outerList">
<li class="innerElement">Hello</li>
<li class="innerElement">World</li>
<li class="innerElement">Hello World</li>
</ul>
<script>
$(".outerList").keydown(function () {
console.log("I am the outer ul");
});
$(".innerElement").keydown(function() {
console.log("I am an inner element");
});
</script>
</body>
</html>
and here is the jsFiddle to run it
http://jsfiddle.net/scrbovyr/
Basically I have a content editable UL and I want to catch the enter key and pass in my own custom function. But I need to know which LI element the keydown event was thrown on. And as shown in the demo, I can only seem to tie a keydown event listener (or any event listener for that matter) to the outer UL element. Is there a way to attach the keydown event to each LI? Or is there at least a way to attach it to the UL but still tell which child it came from?
Thanks in advance, let me know if any additional information would help!
You will have to add contenteditable to your li elements in order to achieve that. You are setting contenteditable to your ul element, thus, the event will be binded to that element, you may edit the li elements, but they do not have contenteditable set, so the keyboard events won't be triggered for those elements.
<ul class="outerList">
<li contenteditable class="innerElement">Hello</li>
<li contenteditable class="innerElement">World</li>
<li contenteditable class="innerElement">Hello World</li>
</ul>
And then:
$(".innerElement").keydown(function() {
console.log("I am an inner element");
});
You may check the node at the current selection
If you don't want to make each li a contenteditable element, you may get the element at the current selection or caret position and perform a check against it.
The embedded example shows how you would achieve this using the Web API Interface for contenteditable selections. (I tested this in Chrome, but it may need additional logic to achieve cross-browser compatibility).
It is also worth noting that you can bind some event listeners to the children of a contenteditable element. For example, the click event may be bound to the li elements as you can see in the embedded example.
$(document).ready(function() {
function getCurrentNode() {
var node = window.getSelection().getRangeAt(0).commonAncestorContainer;
return node.nodeType === 1 ? node : node.parentNode;
}
$('.outerList').on('click keyup', function (e) {
var $target = $(getCurrentNode()),
$closest = $target.closest('b');
console.log(e.type);
console.log('I am the outer ul');
console.log($target);
// Optional. Filter by clostest selector.
if ($closest.length) {
console.log('Target matches selector', $closest);
}
});
$('.innerElement').on('click', function (e) {
console.log(e.type);
console.log('I am an inner element');
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul contenteditable class="outerList">
<li class="innerElement">Hello</li>
<li class="innerElement"><i>Hello</i></li>
<li class="innerElement"><b><i>Hello</i></b></li>
<li class="innerElement"><b>Hello</b></li>
<li class="innerElement">Hello</li>
<li class="innerElement">Hello</li>
</ul>

Optimize live elements' selectors in jQuery

I read a lot about optimization in jQuery in some links below:
jQuery Website , Performance
jQuery Best Practices - Greg Franko
jQuery Coding Standards and Best Practices
14 Helpful jQuery Tricks, Notes, and Best Practices
and more ...
But none of them mentioned for .on() caching selectors. I don't know if there is any way to use cached elements in these kind of selectors.
for example I have a lot of these selectors in my script.js file.
$(document).on('click', '.menu li.remove', function(e){ ... });
$(document).on('click', '.menu li.edit', function(e){ ... });
$(document).on('click', '.menu li.action', function(e){ ... });
$(document).on('click', '.menu li.anotherAction', function(e){ ... });
and much more. .menu is a menu and can be anywhere in document, so I can't use specific id container to select it. like this:
$('#sidebar').on('click', '.menu li.action', function(e){ ... });
is there any way to optimize these selectors. Checking for existence maybe, caching .menu if it is possible.
When you need to eek out every last bit of performance, you probably need to ditch abstractions.
If you do your own delegation, you'll certainly see a performance improvement.
Because in the example you gave, all the delegation is identical except for the class name, I'd bind a single handler, put the code in separate functions, and then examine the e.target and its ancestors manually looking for the .menu li. If that's found, then check the class of the li, and invoke the correct handler.
var handlers = {
remove: function() {/*your code*/},
edit: function() {/*your code*/},
action: function() {/*your code*/},
anotherAction: function() {/*your code*/}
};
var targets = Object.keys(handlers);
document.onclick = function(e) {
e = e || window.event;
var li;
var node = e.target || e.srcElement;
var targetClass;
do {
if (!li) {
if (node.nodeName === "LI") {
li = node;
}
} else if (node.className.indexOf("menu") > -1) {
targetClass = li.className
break;
}
} while(node = node.parentNode);
if (!targetClass)
return;
for (var i = 0; i < targets.length; i++) {
if (targetClass.indexOf(targets[i]) > -1) {
handlers[targets[i]].call(li, e);
}
}
}
In the code above, as we traverse up from the e.target, we first check to see if we're on an li. If so, grab it and continue one.
As we continue, we no longer need to check for li elements, but we now need to check for an element with the menu class. If we find one, we grab the class name of the li we previously found and then halt the loop.
We now know we have our menu li.someClass element. So then we can use the class that we found on the li to look up the proper function to invoke from our list of functions we made above.
You should note that my .indexOf() class testing is ad hoc, and could result in false positives. You should improve it. Also, the code needs more tweaking since we're caching the li without knowing if it actually has a class that we're interested in. That should be fixed as well.
I'll leave it to you to add the necessary tweaks if you desire. :-)
I personally think you are worrying about speed where speed is not an issue.
If the menus are not loaded dynamically, there is nothing stopping you from combining delegated event handlers with normal jQuery selectors to target more of the closer elements (e.g. your .menu class):
e.g.
$('.menu').on('click', 'li.remove', function(e){ ... })
.on('click', 'li.edit', function(e){ ... })
.on('click', 'li.action', function(e){ ... })
.on('click', 'li.anotherAction', function(e){ ... });
This will create a handler on each menu (so closer to the elements).
If your menus are dynamically loaded, then your existing code is perfectly fine, as my understanding is that delegated event handlers only apply the selector argument to the elements in the bubble chain. If that is the case, delegated events will be pretty darn fast anyway. Certainly faster than you can click your mouse! I have never had speed issue with delegated event handlers and I probably overuse them in my plugins (I always assume dynamic content in those).

jQuery nav menu doesn't update first element

I'm trying to build a custom navigation menu with 3 options. Initially, only the active option is visible. Clicking on the active option shows the other options, and upon clicking on another one, it is prepended at the beginning of the menu and the other list items are once again hidden.
// html
<ul>
<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>
</ul>
// css
li {
display: block;
}
li:not(:first-child) {
display: none;
}
// js
$(function(){
$('li:first-child').on('click', function(){
$(this).siblings().toggle()
});
$('li').not(':first-child').on('click', function(){
$(this).prependTo('ul')
$(this).siblings().hide()
});
});
http://jsfiddle.net/H85Yj/
However, the only issue is that after it executes once, it won't run again. I'm guessing that the li:first-child still remains as the first option. Any way I can work around this?
The (undocumented, so far as I can see, in the API for prependTo()) problem you appear to be experiencing is that, once you move the li element from the HTML, the event-binding is not transferred with them; therefore clicking on the li no longer triggers an event. The easiest way around that is to bind the events to the parent ul element, and handle the events there (as the click events bubble up through the DOM and are acted upon by the ancestor).
Therefore, I'd suggest:
$('ul').on('click', 'li', function(){
var self = $(this),
siblings = self.siblings();
if (siblings.filter(':visible').length) {
self.prependTo(self.parent());
siblings.hide();
}
else {
siblings.toggle();
}
});
JS Fiddle demo.
Although, on reflection, the following seems more simple:
$('ul').on('click', 'li', function(){
var _self = $(this);
if (_self.is(':first-child')) {
_self.siblings().toggle();
}
else {
_self.prependTo(_self.parent()).siblings().hide();
}
});
JS Fiddle demo.
Note that I've adjusted the CSS a little, too (to use simple CSS rather than SCSS); setting the display: none as the default rule for the li elements, and display: block for the li:first-child element (as opposed to the needlessly-complex :not(:first-child) rule you used originally).
References:
:first-child selector.
:visible selector.
filter().
is.
on().
parent().
prependTo().
siblings().

Mootools `Events` works just on first `click`, after stops working. Why?

Mootools Events works just on first click, after stops working.
Hope someone have issue for that: http://jsfiddle.net/3j3Ws/
CSS
ul li,li.selected div{
width:22px;
height:22px;
display:block;
background:#000;
color:#fff;
text-align:center;
border-radius:3px;
}
ul#list{
display:none;
opacity:0;
float:left;
}
HTML
<ul id="option">
<li class="selected" id="a">a</li>
<ul id="list">
<li id="b">b</li>
<li id="c">c</li>
<li id="d">d</li>
</ul>
</ul>​
Mootools JavaScript
window.addEvent('domready', function(){
var x = '<div>v</div>';
$$('ul#option li.selected').set('html',x);
var opt = $$('ul#option li.selected div');
var d = opt.getStyle('display');
var l = document.id('list');
var list = opt.set('morph').addEvents({
click:function(){
l.store('timerA',(function(){
l.morph({
'display':'block',
'opacity':1
});
$$('ul#option li.selected').setStyle('background-color','#fff');
$$('ul#option li.selected div').destroy();
}).delay(10,this));//$clear(this.retrieve('timerA'));
}
}
);
l.set('morph').addEvents({
mouseleave:function(el){
this.store('timerB',(function(){
this.morph({
'display':d,
'opacity':0
});
$$('ul#option li.selected').removeProperties('style');
$$('ul#option li.selected').set('html',x);
}).delay(500,this));//$clear(this.retrieve('timerB'));
}
});
});​
odd writing style you have.
anyway. it is the destroy. the events are not delegated. i.e. your selector is the first div but that's a physical element that gets a UID and a functional cllback against that.
by doing .destroy() you are removing this div from the dom and even if you reinsert it after, because you don't use event delegation, the event will no longer work (events are part of element storage so destroy removes them too).
check out http://jsfiddle.net/dimitar/3j3Ws/1/ -> proves it can work fine (i added mootools more for easy .show() and .hide() but you can just use .setStyle("display", "none").
alternatively, look at doing an event for document.id("option") as click:relay(div.down) and mod the x html to have class='down' - then the code you have at the moment will keep.
It's most likely this:
$$('ul#option li.selected div').destroy();
At that point, you're deleting the <div>v</div> that you inserted earlier and had attached the click event to.
In the mouseleave later, you do:
$$('ul#option li.selected').set('html',x);
which recreates the div, but has not also reattached the click handler to the new copy.
comment followup:
when you use the .set('html', x), you're replacing the original node with a new one, which also replaces the event handlers. Handlers are attached to an actual node, not to the node's location in the DOM tree.

Categories

Resources