admin管理员组文章数量:1134246
I tried
node.cloneNode(true); // deep copy
It doesn't seem to copy the event listeners that I added using node.addEventListener("click", someFunc);
.
We use the Dojo library.
I tried
node.cloneNode(true); // deep copy
It doesn't seem to copy the event listeners that I added using node.addEventListener("click", someFunc);
.
We use the Dojo library.
Share Improve this question edited Jan 15, 2017 at 16:55 Brett DeWoody 62.7k31 gold badges144 silver badges191 bronze badges asked Mar 14, 2013 at 11:48 Chakradar RajuChakradar Raju 2,8113 gold badges29 silver badges42 bronze badges10 Answers
Reset to default 110cloneNode()
does not copy event listeners. In fact, there's no way of getting hold of event listeners via the DOM once they've been attached, so your options are:
- Add all the event listeners manually to your cloned node
- Refactor your code to use event delegation so that all event handlers are attached to a node that contains both the original and the clone
- Use a wrapper function around
Node.addEventListener()
to keep track of listeners added to each node. This is how jQuery'sclone()
method is able to copy a node with its event listeners, for example.
This does not answer the question exactly, but if the use case allows for moving the element rather than copying it, you can use appendChild, which will preserve the event listeners. For example:
function relocateElementBySelector(elementSelector, destSelector) {
let element = document.querySelector(elementSelector);
let destElement = document.querySelector(destSelector);
// appending an existing element simply moves it.
destElement.appendChild(element);
}
Event Delegation example.
After reading Tim Down's answer, I found delegated events are very easy to implement, solving a similar problem I had. I thought I would add a concrete example, although it's in JQuery not Dojo.
I am re-skining an application in Semantic UI, which requires a small piece of JS to make the message close buttons work. However the messages are cloned from an HTML template tag using document.importNode
in a library. This meant even if I did attach the event handlers to the template in the new HTML, they are lost during the cloning.
I cannot do Tim's option 1, to simply re-attach them during cloning as the messaging library is front-end framework agnostic. (Interestingly my previous front-end was in Zurb Foundation which uses a "data-closable" attribute, the functionality of which does survive the cloning process).
The normal event handling suggested was like this:
$('.message .close').on('click', function() {
$(this)
.closest('.message')
.transition('fade');
});
The problem being ".message" at app-load only matches the single template, not the actual messages which arrive later over web-sockets.
Making this delegated, meant attaching the event to the container into which the messages get cloned <div id="user-messages">
So it becomes:
$('#user-messages').on('click', '.message .close', function() {
$(this)
.closest('.message')
.transition('fade');
});
This worked immediately, saving any complex work like the third option of wrapping the event subs.
The Dojo equivalent looks pretty similar in concept.
const _originAddEventListener = HTMLElement.prototype.addEventListener;
const _originRemoveEventListener = HTMLElement.prototype.removeEventListener;
const _originCloneNode = HTMLElement.prototype.cloneNode;
const _eventListeners = [];
const getEventIndex = (target, targetArgs) => _eventListeners.findIndex(([elem, args]) => {
if(elem !== target) {
return false;
}
for (let i = 0; i < args.length; i++) {
if(targetArgs[i] !== args[i]) {
return false;
}
}
return true;
});
const getEvents = (target) => _eventListeners.filter(([elem]) => {
return elem === target;
});
const cloneEvents = (source, element, deep) => {
for (const [_, args] of getEvents(source)) {
_originAddEventListener.apply(element, args);
}
if(deep) {
for(const i of source.childNodes.keys()) {
const sourceNode = source.childNodes.item(i);
if(sourceNode instanceof HTMLElement) {
const targetNode = element.childNodes.item(i);
cloneEvents(sourceNode, targetNode, deep);
}
}
}
};
HTMLElement.prototype.addEventListener = function() {
_eventListeners.push([this, arguments]);
return _originAddEventListener.apply(this, arguments);
};
HTMLElement.prototype.removeEventListener = function() {
const eventIndex = getEventIndex(this, arguments);
if(eventIndex !== -1) {
_eventListeners.splice(eventIndex, 1);
}
return _originRemoveEventListener.apply(this, arguments);
};
HTMLElement.prototype.cloneNode = function(deep) {
const clonedNode = _originCloneNode.apply(this, arguments);
if(clonedNode instanceof HTMLElement){
cloneEvents(this, clonedNode, deep);
}
return clonedNode;
};
Only inline attributes would work here which are heavily, heavily discouraged because of how misused they are. That said, you can have elements bind to the same event listener.
The proper way with Web Components (and shadow root) would look like and what we would want to replicate:
onButtonClick(event) {
console.log('onButtonClick', { event, this: this });
}
/* Or constructor */
connectedCallback() {
this.shadowRoot.getElementById('button')
.addEventListener(this.onButtonClick);
}
It's efficient because you don't create function per element like you would with .addEventListener(() => this.onButtonClick)
. 1000 buttons would attach to the same function instead of creating a new function per button.
To convert that to inline would look like this:
<button onclick="this.getRootNode().host.onButtonClick.call(this, event)">
Is it ugly? Yes. But does it work? Also, yes. In this case there's no need for JS to have find the element and instruct the browser to create an event handler. The inline onclick
does that for you. I will note that are creating a new function for each and every element, instead of them all sharing one.
This is what @JeromeJ was describing in a comment. Create the initial element using this HTML code.
<DIV ONCLICK="doSomething(this)">touch me</DIV>
When you clone this element the result will have the same handler, and "this" will point to the cloned element.
It would be great if the ONCLICK handler could easily be added in JavaScript. This approach means that you have to write some of your code in HTML.
Cloning a node copies all of its attributes and their values, including intrinsic (inline) listeners. It does not copy event listeners added using addEventListener() or those assigned to element properties (e.g., node.onclick = someFunction). Additionally, for a element, the painted image is not copied.
source: MDN (https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode).
Though it's impossible to clone a node altogether with its event listeners, it's possible to pass the event back to the origin element, to get similar effects.
Here's an implementation of this idea: https://gist.github.com/std-microblock/a412a6c27a6dc0e49c091ce64e96ae6b
You can test it through this script:
// run in any Stackoverflow question pages with answers.
// This would create a perfect delegate dom of answers element, all the buttons should be interactable.
// sadly, the keyboard input is not working
const win = window.open('about:blank', undefined, 'popup');
for(const style of document.querySelectorAll('style, link'))
win.document.head.appendChild(style.cloneNode(true));
win.document.body.appendChild(createElementDelegate(document.querySelector("#answers"), {cloneParent:true, syncProps: true}))
<script src="https://gist.githubusercontent.com/MicroCBer/a412a6c27a6dc0e49c091ce64e96ae6b/raw/9553ffa4af0d79980212d7fdf9c884cdbf3f3d16/cloneEx.js"></script>
A few years have passed, so it's time for another answer on this topic :)
So far I found this to be working. (Note, that I'm still very much junior in programming. Go easy on me)
You want to copy a DOM element with all its event listeners.
- create a deep clone
[node.cloneNode(true)][1]
- copy over event listeners
- insert the new element into DOM
The first one is easy. The second step has a solution built into Chrome, and as it turns out, an amazing dude turned it into a JS library to use anywhere. The syntax is slightly different but even better. You can get specific listeners easily. Finally inserting the clone in the DOM is again simple.
let c = 0;
// Event handler
let handleClick = (el)=>{
let newElement = el.cloneNode(true);
// Change clone's ID to avoid ID duplication in DOM
let newId = 'hasBeenCopied' + c;
newElement.setAttribute('id', newId);
c++;
let listeners = toBeCopied.getEventListeners();
// getEventListeners returns an object of arrays for each event type
// {click: Array(1)}
Object.keys(listeners).forEach((type)=>{
// iterate over the various types
listeners[type].forEach(l=>{
// then over all different listeners in the arrays
// and add the listeners one-by-one to the clone
newElement.addEventListener(type,l.listener);
})
});
parent.append(newElement);
};
// Element magic
let parent = document.getElementById('parent');
let toBeCopied = document.getElementById('toBeCopied');
// Event listeners
toBeCopied.addEventListener('click',()=>{handleClick(toBeCopied);});
div {
border: 1px solid #112233;
padding: 1rem;
margin: 1rem;
}
.clickable {cursor:pointer};
<script src="https://cdn.jsdelivr.net/gh/colxi/getEventListeners/src/getEventListeners.min.js"></script>
<div id="parent">
<div id="toBeCopied" class="clickable">this is gonna be copied</div>
</div>
I know I'm late to the party but this a solution that worked for me:
const originalButtons = original.querySelectorAll<HTMLElement>('button');
const cloneButtons = clone.querySelectorAll<HTMLElement>('button');
originalButtons.forEach((originalButton: HTMLElement, index: number) => {
cloneButtons[index].after(originalButton);
cloneButtons[index].remove();
});
本文标签: javascriptHow to copy a DOM node with event listenersStack Overflow
版权声明:本文标题:javascript - How to copy a DOM node with event listeners? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736854060a1955635.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论