20

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.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Matt
  • 41,216
  • 30
  • 109
  • 147
  • Interesting question. FWIW, it should be easy to test: Nest two elements, watch (separately) for clicks on both of them, in the click handler on the child, remove the child (`this.parentNode.removeChild(this)`), and see if the event bubbles up to the parent. (Running out the door or I'd've tried it myself.) – T.J. Crowder Apr 28 '10 at 20:29
  • I decided to stick around and try it, see below. – T.J. Crowder Apr 28 '10 at 20:48

3 Answers3

24

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>
Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    @T.J. Brilliant detail and summary, the perfect answer, shame I can't give another +1 lol. – Andy E Apr 29 '10 at 13:31
  • I hit this problem again today, and finally figured out what was going on! Thought you'd be interested. Posted my own answer below. – Matt Jan 05 '11 at 17:13
6

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.

Matt
  • 41,216
  • 30
  • 109
  • 147
  • 1
    Interesting stuff. It also appears Internet Explorer has it right here; check the [DOM3 Events draft spec](http://dev.w3.org/2006/webapi/DOM-Level-3-Events/html/DOM3-Events.html#event-type-click) -- *"The click event may be preceded by the mousedown and mouseup events on the same element, disregarding changes between other node types (e.g., text nodes)."* -- basically saying that as long as the topmost element under the mouse remains the same the click event should fire. If the innerHTML changes, however, it shouldn't. – Andy E Jan 06 '11 at 10:26
  • Found this question again 2 years later and this issue is fixed in the latest Firefox (though your Fiddle needs a bit of work) but not Webkit, sadly. Might be worth posting a bug report! – Andy E Feb 11 '13 at 23:58
  • Thank you so much for your solution and explanation. I've been troubleshooting this behavior for a few days on my end, I had a frequently changing DOM element and onclick events sometimes were just not firing. Using onmouseup instead of onclick now and they fire 100% of the time. I guess that if the DOM element for mousedown and mouseup don't match, it would cause ambiguity and the browsers preferred not firing onclick at all if it was going to be ambiguous, but still fire mouseup and mousedown for the different versions of the updating DOM. – Wadih M. Aug 13 '21 at 02:59
5

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.

Andy E
  • 338,112
  • 86
  • 474
  • 445
  • @Andy: Actually, no, it depends on the browser. I decided to stick around and try it, see my answer. – T.J. Crowder Apr 28 '10 at 20:47
  • @T.J. lol I knew I should have tested IE. It pays to be thorough, I suppose, +1 for you! Still, I can't understand why IE would do this (although it doesn't really surprise me that much), it goes against all logic if you ask me. Like I said in my answer, with `removeChild`, the element isn't deleted entirely, just detached from the DOM tree, and can be put back in. It just makes no sense to cancel the event. – Andy E Apr 28 '10 at 21:57
  • @Andy: Yeah, *always* test with IE, so many things are different. :-) I can sort of see their interpretation, using this (entirely hypothetical logic): 1. Dispatcher receives event 2. Dispatcher dispatches event on element 3. Handler removes element 4. Dispatcher checks `cancelBubble` flag and it's false 5. Dispatcher looks at `element.parentNode` -- but it's `null`; stop dispatching. Whereas the other browsers seem to grab the entire ancestry *before* they start dispatching the event. Barring the specification being clear about what should happen, I don't see either behavior as wrong. :-) – T.J. Crowder Apr 29 '10 at 07:21
  • @T.J. yeah, the w3c appears to be on my side ;-) I've added a quote from the level 2 spec just for thread completion. I have to say I do think it makes more sense this way, event bubbling is there for a reason and it shouldn't be like the event "never happened" for the the chain of ancestors just because the element is removed from the tree. – Andy E Apr 29 '10 at 09:45
  • 1
    @Andy: Excellent, thanks for the quote and reference! Poor Microsoft. :-) And that seems like the best way to define the bubbling, too, glad they did that in DOM2. – T.J. Crowder Apr 29 '10 at 11:01
  • 1
    Nice find on the DOM2 event specification. +1 – Matt Apr 29 '10 at 13:49
  • Thanks @Matt, it turned out to be a very interesting question :-) – Andy E Apr 29 '10 at 14:00
  • @Andy E: I hit this problem again today, and finally figured out what was going on! Thought you'd be interested. Posted my own answer below. – Matt Jan 05 '11 at 17:14