ISSUE-35 - Public View

DOM3EV: dispatchEvent vs event state (ISSUE-20 take two)

State:
CLOSED
Product:
DOM 3 Events
Raised by:
Bjoern Hoehrmann
Opened on:
2006-03-05
Description:
In the now resolved ISSUE-20 we've discussed some of the interactions 
between Event.preventDefault, Event.stopPropagation() and dispatchEvent, 
in particular what happens if an event object is dispatched after an 
initial dispatch has been completed. It seems additional clarification 
is necessary.

For the purposes of this issue, an implementation is assumed to dispatch 
event objects using dispatchEvent() and an event is considered to be 
currently beeing dispatched from the point where control is passed to 
the dispatchEvent() implementation and when the method returns.

stopPropagation, stopImmediatePropagation, and preventDefault modify the 
state of the event object. When these are called on an event object that 
is currently beeing dispatched, this yields in well-defined results. The 
concern is what happens

a) if these are called before initial dispatch of the object, for example

  <form id="test" action="data:,SUBMITTED"></form>
  <script>
    var evt = document.createEvent("Events");
    evt.initEvent('submit', true, true);
    evt.preventDefault();
    document.getElementById('test').dispatchEvent(evt);
  </script>

and

  <form id="test" action="data:,SUBMITTED"></form>
  <script>
    var evt = document.createEvent("Events");
    evt.initEvent('submit', true, true);
    evt.stopPropagation();
    document.getElementById('test').dispatchEvent(evt);
  </script>

For the first case, some implementations dispatch the event as expected, 
and the effect is as if preventDefault was called from some listener, 
i.e., the default action is prevented. In the second case, some 
implementations dispatch the event by not triggering any listeners and 
performing the default action. This is about what I would expect from 
the current text.

b) for dispatchEvent(an event currently beeing dispatched)

this is well-defined in the draft, you get an exception. This is new in 
DOM Level 3 Events and not so widely implemented, some implementations 
simply ignore the method call. I think what the draft says is fine.

c) when dispatching a dirty event object

  var evt = document.createEvent('MouseEvent');
  evt.initEvent('click', ...);
  for (...)
    target.dispatchEvent(evt);

is one case for this. If one of the click listeners prevent the default 
action of the event or stop it's propagation, would dispatchEvent reset 
this state? Implementations appear to be most inconsistent for this case 
and we resolved that yes, dispatchEvent would reset the state. Concerns 
have been expressed regarding the asymetrie with how this works for 
event objects that have not been dispatched yet.

One thing to note here is that the current draft notes that calling 
initEvent() on an event object that is currently beeing dispatched, or 
has been in the currently beeing dispatched state at least once, has no 
effect. So if neither initEvent nor dispatchEvent reset those states,

  <p id='test'>0</p>
  <script>
    var test = document.getElementById('test').firstChild;
    function update(evt) {
      test.nodeValue = String(Number(test.nodeValue) + 1);
      evt.stopPropagation();
    }
    document.addEventListener('bh-update', update, false);
    var update = document.createEvent('Events');
    update.initEvent('bh-update', true, true);
    setInterval(function(){document.dispatchEvent(update)}, 500);
  </script>

would call update() only once. This is indeed the case for e.g. two 
weeks old experimental versions of Firefox and Opera9, though removing 
the evt.stopPropagation() would yield in the same result in Firefox, 
which insists that initEvent is called again. So if this is changed to

    setInterval(function(){
      update.initEvent('bh-update', true, true);
      document.dispatchEvent(update)}, 500);

it would work as "expected" in both browsers. Unfortunately these 
implementations ignore that initEvent must have no effect, so if you have

    setInterval(function(){
      document.dispatchEvent(update);
      update.initEvent('bh-update2', true, true);
    }, 500);

they would change the Event.type to bh-update2 and never call the 
listener again. It's the same if initEvent gets called while the event 
is currently beeing dispatched, so

  <p id='test'>0</p>
  <script>
    var test = document.getElementById('test').firstChild;
    function update(evt) {
      test.nodeValue = String(Number(test.nodeValue) + 1);
      evt.initEvent('bh-update2', true, true);
    }
    document.addEventListener('bh-update', update, false);
    var update = document.createEvent('Events');
    update.initEvent('bh-update', true, true);
    setInterval(function(){document.dispatchEvent(update)}, 500);
  </script>

would increment to 1 and then stop... A slightly different case is

  <body>
  <p id='test'>0</p>
  <script>
    var test = document.getElementById('test').firstChild;
    function update(evt) {
      test.nodeValue = String(Number(test.nodeValue) + 1);
      evt.stopPropagation();
      evt.initEvent('bh-update', true, true);
    }
    document.addEventListener('bh-update', update, false);
    var update = document.createEvent('Events');
    update.initEvent('bh-update', true, true);
    setInterval(function(){document.body.dispatchEvent(update)},500);
  </script>
  </body>

this proofs that initEvent does not reset the propagation state, the 
listener would be triggered just once. Then, for

  <body>
  <p id='test'>0</p>
  <script>
    var test = document.getElementById('test').firstChild;
    function update(evt) {
      test.nodeValue = String(Number(test.nodeValue) + 1);
      evt.initEvent('bh-update', false, false);
    }

    document.body.addEventListener('bh-update', update, false);
    document.addEventListener('bh-update', update, false);

    var update = document.createEvent('Events');
    update.initEvent('bh-update', true, true);
    setInterval(function(){document.body.dispatchEvent(update)},500);
  </script>
  </body>

we can take note of the fact that Firefox does [0,1,2] and Opera9 does 
[0,1,2,3,...], hmm, and

  <body>
  <p id='test'>0</p>
  <script>
    var test = document.getElementById('test').firstChild;
    function update(evt) {
      if (evt.currentTarget === document)
        test.nodeValue = String(Number(test.nodeValue) + 1);
      evt.initEvent('bh-update', false, false);
    }

    document.body.addEventListener('bh-update', update, false);
    document.addEventListener('bh-update', update, false);

    var update = document.createEvent('Events');
    update.initEvent('bh-update', true, true);
    setInterval(function(){document.body.dispatchEvent(update)},500);
  </script>
  </body>

gives [1] in Firefox and [0] in Opera9. I guess the interpretation for 
that is that initEvent can be used to emulate stopPropagation() in 
Opera9 but can't be used that way in Firefox...

Now, these features aren't that widely used and implemented and the 
cases mentioned above are quite some edge cases, so we are probably 
relatively free to pick a sensible solution for this. I note that if 
implementations keep some internal state like whether the event has
been generated by the implementation, dispatchEvent would have to
reset at least this state, otherwise it would be possible to trick
the user into entering [/etcpaswd] and submit that to a file upload
control if that's allowed at all, for example.

So one way to resolve this would be to say there are two states, when 
the event is currently beeing dispatched and when it's not beeing 
dispatched. If it's not beeing dispatched, DOM applications can mutate 
the event as they like and dispatchEvent would honor that. If it is 
beeing dispatched, initEvent() has no effect, preventDefault() etc work 
as they should. dispatchEvent would, after all listeners have been 
processed, i.e., immediately before it returns, reset the propagation 
and .defaultPrevented states, and return. This would be roughly what 
implementations do, what we've resolved, and what you might expect, 
while retaining some level of sanity.

The downside of that would be that Event.defaultPrevented is always 
false immediately after dispatchEvent() returns, so applications would 
always have to check the return value of dispatchEvent to decide whether 
to perform the default action; this could easily be addressed by a note 
for .defaultPrevented though.

Note: Some of these links may be accessible only to W3C Members.

Related emails:
  1. ISSUE-35: DOM3EV: dispatchEvent vs event state (ISSUE-20 take two) (from dean+cgi@w3.org on 2006-03-05)
  2. Re: ACTION-76: Build test case for understanding behaviour of redispatching cancelled and stopped events (from derhoermi@gmx.net on 2006-03-15)
  3. Re: DOM-Events: Non-bubbleable means non-capturable? (from derhoermi@gmx.net on 2006-03-24)
  4. Minutes, face to face meeting (from chaals@opera.com on 2006-05-15)

Related notes:

No additional notes.