Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

If you delete a DOM element, do any events that started with that element continue to bubble?

What behavior should I expect if I delete a DOM element that was used to start an event bubble, or whose child started the event bubble - will it continue to bubble if the element is removed?

For example - lets say you have a table, and want to detect click events on the table cells. Another piece of JS has executed an AJAX request that will eventually replace the table, in full, once the request is complete.

What happens if I click the table, and immediately after the table gets replaced by a successful completion of an AJAX request? I ask because I am seeing some behavior where the click events don't seem to be bubbling - but it is hard to duplicate.

I am watching the event on a parent element of the table (instead of attaching the event to every TD), and it just doesn't seem to reach it sometimes.

EDIT: Encountered this problem again, and finally got to the root of it. Was not a event-bubbling issue at all! See my answer below for details.

like image 253
Matt Avatar asked Apr 28 '10 20:04

Matt


3 Answers

Empirically: It depends on what browser you're using; IE cancels the event, everything else (as far as I can tell) continues it. See the test pages and discussion below.

Theoretically: Andy E's head helpfully found that DOM2 says the event should continue because bubbling should be based on the initial state of the tree. So the behavior of the majority is correct, IE's on its own here. Quelle surprise.

But: Whether that relates to what you're seeing is another question indeed. You're watching for clicks on a parent element of the table, and what you suspect is that very rarely, when you click the table, there's a race condition with an Ajax completion that replaces the table and the click gets lost. That race condition can't exist within the Javascript interpreter because for now, Javascript on browsers is single-threaded. (Worker threads are coming, though — whoo hoo!) But in theory, the click could happen and get queued by a non-Javascript UI thread in the browser, then the ajax could complete and replace the element, and then the queued UI event gets processed and doesn't happen at all or doesn't bubble because the element no longer has a parent, having been removed. Whether that can actually happen will depend a lot on the browser implementation. If you're seeing it on any open source browsers, you might look at their source for queuing up UI events for processing by the interpreter. But that's a different matter than actually removing the element with code within the event handler as I have below.

Empirical results for the does-bubbling-continue aspect:

Tested Chrome 4 and Safari 4 (e.g., WebKit), Opera 10.51, Firefox 3.6, IE6, IE7, and IE8. IE was the only one that cancelled the event when you removed the element (and did so consistently across versions), none of the others did. Doesn't seem to matter whether you're using DOM0 handlers or more modern ones.

UPDATE: On testing, IE9 and IE10 continue the event, so IE noncompliance with spec stops at IE8.

Test page using DOM0 handlers:

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<title>Test Page</title>
<style type='text/css'>
body {
    font-family: sans-serif;
}
#log p {
    margin:     0;
    padding:    0;
}
</style>
<script type='text/javascript'>
window.onload = pageInit;

function pageInit() {
    var parent, child;

    parent = document.getElementById('parent');
    parent.onclick = parentClickDOM0;
    child = document.getElementById('child');
    child.onclick = childClickDOM0;
}

function parentClickDOM0(event) {
    var element;
    event = event || window.event;
    element = event.srcElement || event.target;
    log("Parent click DOM0, target id = " + element.id);
}

function childClickDOM0(event) {
    log("Child click DOM0, removing");
    this.parentNode.removeChild(this);
}

function go() {
}

var write = log;
function log(msg) {
    var log = document.getElementById('log');
    var p = document.createElement('p');
    p.innerHTML = msg;
    log.appendChild(p);
}

</script>
</head>
<body><div>
<div id='parent'><div id='child'>click here</div></div>
<hr>
<div id='log'></div>
</div></body>
</html>

Test page using attachEvent/addEventListener handlers (via Prototype):

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<title>Test Page</title>
<style type='text/css'>
body {
    font-family: sans-serif;
}
#log p {
    margin:     0;
    padding:    0;
}
</style>
<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js'></script>
<script type='text/javascript'>
document.observe('dom:loaded', pageInit);
function pageInit() {
    var parent, child;

    parent = $('parent');
    parent.observe('click', parentClick);
    child = $('child');
    child.observe('click', childClick);
}

function parentClick(event) {
    log("Parent click, target id = " + event.findElement().id);
}

function childClick(event) {
    log("Child click, removing");
    this.remove();
}

function go() {
}

var write = log;
function log(msg) {
    $('log').appendChild(new Element('p').update(msg));
}
</script>
</head>
<body><div>
<div id='parent'><div id='child'>click here</div></div>
<hr>
<div id='log'></div>
</div></body>
</html>
like image 80
T.J. Crowder Avatar answered Nov 08 '22 04:11

T.J. Crowder


It's been quite some time since I originally posted this question. Although T.J.Crowder's answer was very informative (as was Andy E's), and told me it should work, I continued to see a problem. I put it aside for some time, but revisited it today when I encountered the same issue again in another web application.

I played around with it for a while, and I came to realize how to duplicate the problem every-time (at least in FF3.6 and Chrome 8). The problem wasn't that the event bubble was getting cancelled, or lost when the DOM element was removed. Instead, the problem is that if the element is changed between mousedown and mouseup, the 'click' does not fire.

Per the Mozilla Development Network:

The click event is raised when the user clicks on an element. The click event will occur after the mousedown and mouseup events.

So, when you have a DOM element that is changing at all, you can encounter this problem. And I erroneously believed the event bubble was being lost. It just happens that if you have a frequently updating element, you see it more often (which is my case) and are less likely to pass it off as a fluke.

Additional testing (see the example on jsfiddle) shows that if one clicks, holds the button down and waits for the DOM element to change, and then releases the button, we can observe (in the jquery live context):

  • The 'click' event does not fire
  • The 'mousedown' event fires for the first node
  • The 'mouseup' event fires for the updated node

EDIT: Tested in IE8. IE8 fires mousedown for first node, mouseup for updated node, but does in fact fire 'click', using the updated node as the event source.

like image 36
Matt Avatar answered Nov 08 '22 05:11

Matt


Yes, it should continue to propagate. Events have no real attachment to the event they fired on, except for the target property. When you remove the element, the internal code propagating the event should not have any "awareness" that the original element has gone from the visible document.

As an aside, using removeChild will not delete an element right away, it just detaches it from the document tree. An element should only be deleted/garbage collected when there are no references to it. Therefore, it's possible that the element could still be referred to via the event.target property and even re-inserted before being garbage collected. I haven't tried it though, so it's just speculation.


T.J. Crowder's comment made me decide to knock up a quick example. I was right on both counts, it does bubble and you can still get a reference to the removed node using event.target.

http://jsbin.com/ofese/2/


As T.J. discovered, that is not the case in IE. But the DOM Level 2 Events specification does define it as correct behavior [emphasis mine]:.

Events which are designated as bubbling will initially proceed with the same event flow as non-bubbling events. The event is dispatched to its target EventTarget and any event listeners found there are triggered. Bubbling events will then trigger any additional event listeners found by following the EventTarget's parent chain upward, checking for any event listeners registered on each successive EventTarget. This upward propagation will continue up to and including the Document. EventListeners registered as capturers will not be triggered during this phase. The chain of EventTargets from the event target to the top of the tree is determined before the initial dispatch of the event. If modifications occur to the tree during event processing, event flow will proceed based on the initial state of the tree.

like image 32
Andy E Avatar answered Nov 08 '22 04:11

Andy E