Clone div with all nested elements along with events in pure javascript - javascript

I am trying to clone a div which has input fields.
But the eventListeners are not being fired.
Even though I am doing a deep clone in the following way -
var rows = document.querySelectorAll('.row');
var dupNode = rows[0].cloneNode(true);
sheet.appendChild(dupNode);
Here is the Demo
Each input has a click event and the cloned inputs are not registering with the click event. What am I missing ?

The simplest (and the most effective) thing to do is to bind single event listener on the #sheet container and benefit from event bubbling. In this case you can append as many new elements as you wish and will never need to bind events to them:
document.querySelector('#sheet').addEventListener('click', function(e) {
var target = e.target;
if (target.tagName == 'INPUT') {
alert("clicked");
}
}, false);
A little tricky part is that you need to check on what element event occurred and execute your code on those only you are interested in.
Demo: http://jsbin.com/mejegaqasu/1/edit?html,js,output

Event handlers are not copied during cloning. You will need to delegate the event handlers if you want them to work everywhere in the DOM after cloning.
Event delegation http://davidwalsh.name/event-delegate
cloneNode documentation https://developer.mozilla.org/en-US/docs/Web/API/Node.cloneNode

Related

Add a custom property to an existing DOM event

Is it ok to add a custom property to an existing DOM event? I would like to "mark" click events in a leaf element (say, a <span>) and catch the event (after bubbling) in an ancestor (say, <body>) and take a decision based on that mark.
Sure you can add new properties to an event.
If I understand you correctly you are looking for a way to uniquely identify a specific event so you can de-dupe or count unique events or whatever.
For instance if you have one event handler handling some events on different elements and you want to make sure you handle each event only once. If an event bubbles you might handle the same event on multiple elements. Some events might bubble, some might not (e.g. because another handler called Event.stopPropogation() lower in the dom tree. Maybe you don't have that much control over where you attach your event handlers and have to rely on bubbling in some cases.
Example...
<div id="parent">
<div id="child"></div>
</div>
var parentEl = document.querySelector('#parent'),
childEl = document.querySelector('#child');
child.addEventListener('click', function (e) {
var rand = ''+Math.floor(Math.random()*1000000);
console.log('child', rand);
e.custField = rand;
});
parent.addEventListener('click', function (e) {
console.log('parent', e);
});
child.click();
Downside of this is that you are polluting someone else's objects that might be handled by code that isn't your own. Not usually a great idea.
Alternatively
1) Use Event.timeStamp
Your event handler can keep a cache of Event.timeStamp and use that to de-dupe handled events.
var eventHandler = (function () {
var eventTimeStampCache = {};
return function (evt) {
if (evt.timeStamp in eventTimeStampCache) {
console.log('Ignore this event.', evt);
return;
}
eventTimeStampCache[evt.timeStamp] = true;
console.log('Handle this event.', evt);
};
})();
child.addEventListener('click', eventHandler);
parent.addEventListener('click', eventHandler);
child.click();
2) Use CustomEvent
If you are firing the events in the first place, use a CustomEvent that is all your own and feel free to make custom APIs and add event listeners that listen for your custom event.
I'd suggest firing a CustomEvent in response to the standard event except you have the same problem of knowing if this has been done already or not.
You can add a dataset item to the element called "data-xxx"
<span id="id" data-myvar="myvalue"></span>
And then later on
console.log( document.getElementbyID('id').dataset.myvar ) // myvalue

How does the $html.appendTo($parentEl) work internally to allow listeners to be attached before the $html is in the DOM?

I always thought that listeners can only be attached to elements in the DOM. If an element was dynamically added then we need to reattach the listeners or attach listeners to a parent element (bubbling) in the first place. However when I use .appendTo() I can somehow attach listeners to an element that is not in the DOM and it still works. How does appendTo change things? Am I making a grave mistake using this method which I will regret later?
Code:
var $html = $("<div class='someElNotInDom'>random text</div>");
attachSomeListeners($html); // ex. click event listener etc.
var $parentEl = $(".someElementInDom").eq(0);
$html.appendTo($parentEl);
// all the listeners work. Why?

Closure event delegation - event listener on DOM parent that covers children/descendants of a given class

In jQuery, you can do the following:
$('#j_unoffered').on('click', '.icon_del', function () {...
This puts one handler on the element j_unoffered that fires if any descendant element with class icon_del is clicked. It applies, furthermore, to any subsequently created icon_del element.
I can get this working fine in Closure where the click is on the element itself.
goog.events.listen(
goog.dom.getElement('j_unoffered'),
goog.events.EventType.CLICK,
function(e) {...
How can I specify a parent event target in Closure that works for its children/descendants in the same way as the jQuery example?
I'm assuming I need to use setParentEventTarget somehow, but I'm not sure how to implement it for DOM events. Most of the documentation I've found pertains to custom dispatch events.
-- UPDATE --
I'm wondering if there is anything wrong with this rather simple solution:
goog.events.listen(
goog.dom.getElement('j_unoffered'),
goog.events.EventType.CLICK,
function(e) {
if (e.target.className.indexOf('icon_del') !== -1) {...
It still leaves this bound to the parent, but e.target allows a work-around. The fifth argument in listen (opt_handler) allows you to bind this to something else, so I guess that's an avenue, too.
I don't know about such possibility too, so I suggest other piece of code:
var addHandler = function(containerSelector, eventType, nestSelector, handler) {
var parent = goog.isString(containerSelector) ?
document.querySelector(containerSelector) :
containerSelector;
return goog.events.listen(
parent,
eventType,
function(e) {
var children = parent.querySelectorAll(nestSelector);
var needChild = goog.array.find(children, function(child) {
return goog.dom.contains(child, e.target);
});
if (needChild)
handler.call(needChild, e);
});
});
Usage:
addHandler('#elem', goog.events.EventType.CLICK, '.sub-class', function(e) {
console.log(e.target);
});
Update:
If you will use this e.target.className.indexOf('icon_del') there will be possibility to miss the right events. Consider a container div with id = container, it has couple of divs with class innerContainer, and each of them contains couple of divs with class = finalDiv. And consider you will add event handler with your code above, which will check e.target for innerContainer class. The problem is when user will click on finalDiv your handler will be called, but the event target will be finalDiv, which is not innerContainer, but contained by it. Your code will miss it, but it shouldn't. My code checks if e.target has nested class or contained by it, so you will not miss such events.
opt_handler can't really help you either, because there might be many nested elements you want to hanlde (which of them will you pass here? maybe all, but not that helpful, you can get them in event handler whenever you want), moreover they can be added dynamically after, so when you add handler you could not know about them.
In conclusion, I think doing such a job in an event handler is justified and most efficient.
What you are referring to is called event delegation
It seems that this is not possible (out of the box) with Google Closure Library; So my recommandation is to use jQuery or another similar event handling library that offers this functionality. If this is not possible or if you wanna do it by hand here's one possible approach (NOTE: this is not for production use)
var delegateClick = function(containerId, childrenClass, handler){
goog.events.listen(goog.dom.getElement(containerId), goog.events.EventType.CLICK, function(event){
var target = event.target;
//console.log(event);
while(target){
if ( target.className && target.className.split(" ").indexOf(childrenClass)!== -1) {
break;
}
target = target.parentNode;
}
if(target){
//handle event if still have target
handler.call(target, event);
}
});
}
//then use it, try this here: http://closure-library.googlecode.com/git/closure/goog/demos/index.html
//..select the nav context
delegateClick( 'demo-list' ,'goog-tree-icon', function(event){console.log(event);})
Here's a more in depth analysis of event delegation
Again, you should use a proven library for this, here are some options: jQuery, Zepto, Bean

jQuery - Register an element in the DOM after setting html()

I have a div, where I set the innerHTML after a button has been clicked:
$('#headerDiv').html('Welcome [<a href=\'javascript:void(0);\' id=\'logout_button\'>Logout</a>]');
However, the new element logout_button isn't registered in the DOM, so I can't capture click events using the traditional $('#logout_button').click().
Is it possible to register logout_button in the DOM just after it's been set with the html() method?
Thanks!
Delegate the event
$('#headerDiv').on('click', '#logout_button', function() {
// Your code
});
This will make sure the event is attached to the dynamically added element by the concept of event bubbling.
If delegation isn't your cup of tea, you can bind your click handler to the button before attaching the button. Do that by creating DOM elements and appending them:
var btn = $('<a />').text('Logout').attr({
"href": "javascript:void(0);",
"id": "logout_button"
}).click(function (e) {
// do logout stuff
e.preventDefault();
return false;
});
$('#headerDiv').append(btn);
This has the added bonus of ensuring you are adding valid elements to the DOM.

Native addEventListener with selector like .on() in jQuery

Does anyone know how jQuery's .on() method can be implemented in native JS? The addEventListener method does not take a child/selector element as a way to filter, and I don't think I have the proper bubbling/capturing knowledge to completely understand what is happening in there. I did consult the source in event.js, where it appears that eventually addEventListener does get used just as it normally does, but I'm not sure I quite grok the source.
If the native method does not provide a mechanism for taking advantage of bubbling and capturing, then does the jQuery .on() function really even have any benefit, or does it just make it look that way? I was under the impression that
.on('parent', '.child', fn(){});
is more efficient than attaching an event to all children individually, but from my interpretation of the source, it's difficult to tell if jQuery is somehow managing this in a way to leads to performance improvement, or if it's just for readability.
Is there a standard methodology for implementing events on a parent that take advantage of bubbling/capture phases for it's child elements, rather than having to attach an event to each individual child?
To perform event delegation natively:
parent.addEventListener('click', function(e) {
if(e.target.classList.contains('myclass')) {
// this code will be executed only when elements with class
// 'myclass' are clicked on
}
});
The efficiency you are referring to has to do with how many event handlers you add. Imagine a table with 100 rows. It is much more efficient to attach a single event handler to the table element then 'delegate' to each row than attach 100 event handlers, 1 to each row.
The reason event delegation works is because a click event actually fires on both the child and the parent (because you're clicking over a region within the parent). The above code snippet fires on the parent's click event, but only executes when the condition returns true for the event target, thus simulating a directly attached event handler.
Bubbling/capturing is a related issue, but you only need to worry about it if the order of multiple event handlers firing matters. I recommend reading further on event order if you are interested in understanding bubbling vs capturing.
The most common benefit of event delegation is that it handles new elements that are added to the DOM after the event handler is attached. Take the above example of a table of 100 rows with click handlers. If we use direct event handler attachment (100 event handlers), then new rows that are added will need event handlers added manually. If we use delegated events, then new rows will automatically 'have' the event handler, because it's technically been added to the parent which will pick up all future events. Read What is DOM Event Delegation, as Felix Kling suggested, for more information.
Adding to the accepted answer: since often the actual event target will be nested within the element you want to bind the listener to it would be better to query the parents (Element.closest() includes the element itself). Also this works with complex CSS selectors instead of a single class only.
<button><span>button with</span><span>multiple click targets</span></button>
function addListener(el, type, callbackFn, selector) {
el.addEventListener(type, e => {
const target = e.target.closest(selector);
if (target) callbackFn.call(target, e);
}, true);
}
addListener(document, "click", e => console.log("clickediclick"), "button");
The answer by #Raine Revere, while concise, does not handle all cases. For example, if .child element contains children of its own, then click on the grandchildren will not trigger the handler. Also, jQuery sets the this execution context to the matched element.
The following snippet handles it correctly.
function on(event, elem, selector, handler) {
elem.addEventListener(event, ev => {
const target = ev.target.closest(selector);
if (target) {
handler.apply(target, arguments)
}
})
}
on('click', document.querySelector('.parent'), '.child', function () { console.log('Clicked ' + this.tagName);});
<div class="parent">
<button class="child">Click Me <i class="icon icon-something">→</i></button>
</div>

Categories