Thursday, April 28, 2016

Who called stopPropagation?

When dealing with mature JavaScript systems these days, there are a lot of moving parts -- frameworks, levels of indirection, shadow DOMs, minified code, and so forth. Today I was debugging an issue in which a third-party component was not able to "hear" an event at the top level of the document, even though the event was being fired. I figured someone was intercepting the event and canceling it, or calling stopPropagation(), or something. I was struggling to figure out who. There was a lot of code to hunt through.

If only I could set a breakpoint on stopPropagation() ... and I could!

If you want to play along, you'll need the following three files:


If you load the HTML page in a browser, you'll see a simple UI. Clicking the button will change the message to "bubbled!" The click event flows up through the DOM until bubbles up to expectingClick; it is handled by the event handler added to expectingClick by script.js and changes the message.

But if the page is loaded with ?mischief as the query string (you can click on the "Make mischief" link to do this), an intervening event handler stops the propagation of the event. So the message is changed to "clicked!" by the inner-most listener, but then never changes to "bubbled!" That's the situation I had to try to figure out.

I was using Google Chrome's Developer Tools to debug the issue. So I could set an event listener breakpoint on the event.


Once I did that, I generated the event and the debugger paused on the entry point of the event listener (line 2 of script.js, in our example). I then created the following function by typing into the Chrome console:
window.breakBefore = (function(was) { return function() { debugger; return was.apply(this,arguments); } })
This function takes an existing function as an argument and creates an equivalent function that adds the debugger keyword before invoking the original.

At that point, I could replace the stopPropagation method of the event with my version (while paused at the breakpoint):

e.stopPropagation = window.breakBefore(e.stopPropagation)

Now, if I unset the click breakpoint and continue using the continue button:


I will reach my breakBefore function, showing the call to stopPropagation, and the stack trace showing the route to the invocation will be displayed!

 

I was surprised it worked, and I'm sure it saved me hours. Hopefully it will do the same for you!