Event propagation doesn't stop #69

Closed
iuriikomarov opened this Issue May 14, 2012 · 8 comments

Comments

Projects
None yet
3 participants

Event.stop() doesn't work, if handler was delegated, not added directly to element.

var handler = function(e){
   e.stop();
}

// event bubbling will be stopped:
bean.add(el, 'click', handler);
// event bubbling will NOT be stopped here:
bean.add(el, '.someclass sometag', 'click', handler);

I'm using bean without ender.js
e.stopPropagation() and e.preventDefault() don't work as well.

Collaborator

rvagg commented May 15, 2012

Thanks for reporting this @ilikefm. Any chance you have time to put in a pull request with a test case for this? No probs if you don't.

Contributor

bartsidee commented Jul 25, 2012

This one is bothering me as wel, had a quick look at it but seems to difficult for a quick fix.

Collaborator

rvagg commented Jul 25, 2012

k, thanks for the +1, sounds like I need to spend some quality time with Bean this week.

Collaborator

rvagg commented Jul 26, 2012

Sooo... I've done a bit of work on this in 0.5-wip, but I suspect the behaviour won't be too different to what you have with Bean 0.4. If you want to see the relevant tests then have a poke around here.

The thing about delegation is that you're only getting the event at the object that you've attached the listener to. This means that doing a stop() (i.e. a stopPropagation()) can only stop the propagation from that element up. You're too late to stop the event at the actual element itself because we're not even listening to it.

If your problem is that you're getting the event triggered twice on events that have been delegated at the same root element (body perhaps), then unfortunately that's not what stop() can deal with, it can only stop propagation further up the DOM tree and also preventDefault() on the element itself. It can't prevent handlers from firing at the same point.

But, there is a new method, stopImmediatePropagation(). It can stop events completely, including at the element on which you're listening, so it's perfect for properly halting an event. Unfortunately it comes in the DOM Level 3 spec, so it's relatively new and doesn't have support in IE8 and below, and, surprisingly, it doesn't appear to be supported in Opera yet either. I've exposed it on the event object in 0.5-wip so you can tinker with it there (or in 0.4 with event.originalEvent.stopImmediatePropagation()). I'm not planning on hooking it up to stop() because of the spotty support which would lead to major inconsistencies in behaviour.

I suspect that jQuery may be able to be a bit trickier about this issue because it reimplements the whole DOM Level 3 Events spec itself so can do of its own internal stoppage. It's interesting to note though that they include a note about live() and delegate(), so it's perhaps not as solid as it could be: http://api.jquery.com/event.stopPropagation/

I'm going to close this issue but if anyone comes up with any bright ideas then feel free to raise them for discussion.

@rvagg rvagg closed this Jul 26, 2012

Contributor

bartsidee commented Jul 27, 2012

yes, I can confirm that calling "stopImmediatePropagation()" will stop the event now. I just made a quick test with two click event listeners. In the both the current and new 0.5-wip bean version the parent element is also trigged even if I call stop(), but with the new call "stopImmediatePropagation" it will work as expected.

Just out of curiosity could you explain a bit more why we can not stop it with the normal calls. I'm aware that in the W3C model we have the choice to listen to the event in the capturing or bubbling phase. But you say we can only stop the propagation from that element up, so in that case only from element -> children and not from element ->parents?

Test code I use:

   test('delegate: should be able to stop delegate event', 1, function () {
    var el1 = document.getElementById('foo')
      , el2 = document.getElementById('bar')
      , el3 = document.getElementById('bang')
      , fn1 = function (e) {
          //e.stopImmediatePropagation(); //this will work
          e.stop()  //will not work
          ok(true, 'triggered orginal div which is delegated')
        }
      ,fn2 = function (e) {
          ok(true, 'triggered parent div higher in DOM tree')
        }
    bean.remove(el1)
    bean.remove(el2)
    bean.remove(el3)
    bean.add(el1, '.bar', 'click', fn2, qwery)
    bean.add(el1, '.bang', 'click', fn1, qwery)
    Syn.click(el3)
  })
Collaborator

rvagg commented Jul 28, 2012

Bean (and pretty much every other framework) only operates on the bubbling phase; though it would be interesting to consider how to make it also handle capturing, Sam Stephenson has an interesting write-up on a use-case of capturing: http://37signals.com/svn/posts/3137-using-event-capturing-to-improve-basecamp-page-load-times

(Sorry if the rest of this is a bit basic, I'm considering writing this up for a post so I'm being a bit verbose.)

So, the story is (considering bubbling only), within the browser an event is fired on an element so all listeners are collected and then called in order. You can preventDefault() (which is the same as returnValue = false on older browsers) to stop the browser from doing what it normally do on that event (process a link click, a keypress, etc.). And, you can stopPropagation() (which is the same as cancelBubble = true on old browsers), but all this does is say to the browser to not bubble up to the parent element in the DOM and repeat the same process. Consider:

<div id="foo">
  <div id="bar">
    <div id="bang"></div>
  </div>
</div>

Attach a listener to #bang and call stopPropagation() and you don't stop other listeners on #bang but you will stop any listeners on #bar and #foo. So if you have multiple listeners on #bang then they all get called.

This is where stopImmediatePropagation() comes in, it's what we often want in more cases than what stopPropagation() gives us, it's just a shame that the support is still a bit sketchy at this stage. stopImmediatePropagation() will halt processing of the event listener list for the current element.

Back to delegation. The power of delegation comes from bubbling, you listen to a parent element for the event you want on child elements and use the selector to determine if it was triggered on the event you were interested in. With a wide enough selector you can listen to events on many elements and handle them with a single listener. In your example, you're attaching the event listener to the top element in both add() calls even though you're saying that you're only interested in events from different children. So, when you do a stop() (hence a stopPropagation()), you're not stopping the event at the child element since it's already gone past that point, you're just stopping it from going further up the tree. This is why stopImmediatePropagation() works, because it's stopping listeners that are attached to the same element.

In summary, the key bit of information is that when you do a delegated add(), you're attaching the listener to the element that you pass in as the first argument, not the element that matches the selector you provide, that matching is performed at the time of the event (which also makes it handy for dynamic content by the way, listen to a fixed parent and collect events from descendent elements that may change).

Have I answered your question?

Contributor

bartsidee commented Aug 2, 2012

Thanks for this, it is clear now :)

Any ETA for the 0.5 release?

Collaborator

rvagg commented Aug 5, 2012

The Ender bridge isn't done yet but I think bean.js is pretty much ready.
I'll have a blog post up soon soliciting feedback before a release and we
may jump to 1.0 cause it's pretty major.

On Thursday, August 2, 2012, Bart vd Ende wrote:

Thanks for this, it is clear now :)

Any ETA for the 0.5 release?


Reply to this email directly or view it on GitHub:
#69 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment