Should the API be forwards compatible? #12

Closed
RByers opened this Issue Jul 3, 2015 · 144 comments

Projects

None yet
@RByers
Member
RByers commented Jul 3, 2015

Is the requirement to use feature detection or a polyfill OK?

Olli Pettay suggests:

...but unfortunately that is not backwards compatible. If we could find a nice backwards compatible syntax.
addEventListener("wheel", listener, true, { preventDefault: "never" }); would behave fine, but is a bit long. Would it be good enough?)

To me the benefit of this doesn't seem worth the cost in verbosity, but I could live with it if there was consensus otherwise.

Another option is discussed in #11

@RByers RByers added the spec label Jul 3, 2015
@RByers RByers changed the title from Should we aim to be backwards compatible? to Should the API be backwards compatible? Jul 6, 2015
@RByers
Member
RByers commented Jul 6, 2015

I'd argue that as long as we're changing cancelable (#2) then backwards compatibility should not be a goal. Developers need to use a polyfill for proper test coverage.

@RByers RByers removed the spec label Jul 27, 2015
@RByers
Member
RByers commented Sep 22, 2015

Even though we're not directly changing cancelable (#2), we concluded that we would still change behavior so this is more than a hint and so full backwards compatibility isn't possible (developers must use a polyfill / feature detection).

@RByers RByers closed this Sep 22, 2015
@RByers RByers changed the title from Should the API be backwards compatible? to Should the API be forwards compatible? Feb 19, 2016
@RByers
Member
RByers commented Feb 19, 2016

There's renewed debate on this point, let's continue the discussion here?

@RByers RByers reopened this Feb 19, 2016
@RByers
Member
RByers commented Feb 19, 2016

Perhaps we should brainstorm a bit about alternatives which are more "forwards compatible"? We can't achieve perfect forwards compatibility while preserving the preventDefault impact we want. But we could design an API where the options are just ignored by older browsers.

Options I see that are largely "forwards compatible":

  1. Just make passive the 4th boolean option: addEventListener(type, handler, useCapture, usePassive). Not compatible with Gecko's non-standard extension, but perhaps that could be broken?
  2. Move the options dictionary to the 4th argument withpassive the only option for now: addEventListener(type, handler, useCapture, options). Still potentially a compat issue for Gecko?
  3. Tweak the method name and always encourage use of a trivial polyfill for forwards compat:
    addListener(type, handler, options). Isn't really any more "forwards compatible".
  4. Define a dummy 4th arg and add a 5th? addEventListener(type, handler, useCapture, mustBeFalse, options)

@smaug---- @jacobrossi @domenic @annevk @dtapuska @foolip thoughts?

@phistuck

Mm... 5. Add a single argument overload that only accepts a dictionary. addEventListener({type: "touchstart", handler: function () {}, capture: false /* or just omit it */, passive: true})
Should be familiar from jQuery.

@RByers
Member
RByers commented Feb 19, 2016

But that's not forwards compatible either - will either do nothing or fail with an exception on older browsers. But, like 3, maybe if the polyfill is a trivial one-liner that's still ok?

@phistuck

Perhaps this is not forward compatible because it requires feature detection (an exception is a very easy feature detection, though), but I think this (having a single dictionary parameter) is the approach the web platform is taking recently. Since (at first at least) developers will be using this for passive: true and not for regular event listeners, this does not seem like a bad idea to me.

@domenic
Collaborator
domenic commented Feb 19, 2016

@RByers I really think the current approach is the best. But if I had to pick one of the alternate approaches, I guess 1 or 2 is not the worst.

I agree that @phistuck's 5 does not solve anything compared to the current approach; it requires just as much workaround/polyfilling/etc. and does not have the apparently-desired property of people being able to write one line of code that is automatically passive/non-capturing in new engines and non-capturing in old engines.

@RByers
Member
RByers commented Feb 19, 2016

Thanks @domenic, the current approach seems better than any of these to me too, but I don't know how to evaluate that beyond just a judgement call / gut impression of the compat risks. Any suggestion for how we could bring more data to the debate? Eg. are there other examples where we've ignored forwards compat beyond the common case of just adding new APIs? Every time we add a new argument to a method, has it been designed such that ignoring the argument is unlikely to be breaking?

@tabatkins may also have input.

@phistuck

And another one (which I actually think I initially preferred, within the intent thread) is the same as my 5. - but with a different name (addListener, or on).

@RByers
Member
RByers commented Feb 19, 2016

And another one (which I actually think I initially preferred, in the intent thread) is the same as my 5. - but with a different name (addListener, or on).

That's my 3, right?

@phistuck

Almost, yes - 3 + 5 -
addListener({type: "touchstart", handler: function () {}, capture: false /* or just omit it */, passive: true})
Or -
on({type: "touchstart", handler: function () {}, capture: false /* or just omit it */, passive: true})

@annevk
Collaborator
annevk commented Feb 20, 2016

I don't see a problem with the current approach. This is no different from any other API change. Browsers should just implement the dictionary syntax.

@jacobrossi

@annevk not all browsers are committed to implementing this and even if all browsers implemented this tomorrow we can't add it to existing extended support browsers (IE11, Firefox ESR, etc.). In the blink-dev thread I outlined how this will likely cause unnecessary compatibility issues for these browsers. We could easily avoid this with an approach like 1 or 2 above.

@annevk
Collaborator
annevk commented Feb 22, 2016

1/2 are both rather terrible. I really don't see how this case is different from any other API extensions.

@jacobrossi

Could you elaborate on why you think they're terrible? It's not clear to me why these are bad options.

What we're talking about here isn't really a typical API extension, it's an API overload. The overloading is what introduces unnecessary compatibility pain.

@annevk
Collaborator
annevk commented Feb 22, 2016

Because boolean arguments are bad (they're fine in a dictionary because the name will always be visible from the caller). Making a boolean argument required for every new feature we're going to add is even worse.

@jacobrossi

I agree that if we were designing this from scratch in 2016 we would only use a dictionary. Unfortunately, browsers have all been shipping this API for years with the boolean argument, which means there are compat issues we have to address.

@annevk
Collaborator
annevk commented Feb 22, 2016

We don't address forward compatibility problems. We never have.

@annevk
Collaborator
annevk commented Feb 22, 2016

And even if we were to start now, preserving that boolean argument is not going to fly.

@foolip
Collaborator
foolip commented Feb 22, 2016

The very first wave of users will have to use feature detection since browsers this will have a minority usage share. Is the concern that adoption in the browsers web developers use will be so quick that they will stop using feature detection before that's actually safe to do in the general population? That sounds like the same situation as with every new feature, or is there a difference?

@smaug----
Collaborator

I would probably go with (2). It happens to also clarify what each of the params are for.
1st param to addEventListener would be 'type', telling what event is being listened for
2nd param would be 'listener' - 'what is listening the event'
3rd param would be 'capture' - 'where in the propagation path the event is being listened for'
4th param then (EventHandlingOption or boolean)[1] telling 'how the event will be handled'

dictionary EventHandlingOption
{
boolean passive;
};

[1] boolean just to be compatible with Gecko

@annevk
Collaborator
annevk commented Feb 22, 2016

@smaug---- if at some point we decide that you want listener to be invoked for anywhere in the event path, what do you do then? Locking the third parameter to "capture" precludes those kind of extensions. Doesn't seem like a good idea.

@RByers
Member
RByers commented Feb 22, 2016

What we're talking about here isn't really a typical API extension, it's an API overload. The overloading is what introduces unnecessary compatibility pain.

@jacobrossi, can you elaborate on precisely why overloading introduces more compatibility pain? Eg. how is this worse than a case where we add a new method with a new name?

@smaug----
Collaborator

@smaug---- if at some point we decide that you want listener to be invoked for anywhere in the event path, what do you do then? Locking the third parameter to "capture" precludes those kind of extensions. Doesn't seem like a good idea.

Why would we add such thing? One can currently already add the same listener to capture and bubble phase. Is there any realistic case where we'd want to change the behavior of the current 3rd param?

@annevk
Collaborator
annevk commented Feb 22, 2016

Yeah, we've had all kinds of ideas: https://gist.github.com/annevk/4475457.

@smaug----
Collaborator

And the use case is?

@annevk
Collaborator
annevk commented Feb 22, 2016

E.g., only wanting to receive events actually targeted at you could be useful for nested controls. Listening to events dispatched on your descendants regardless of whether they bubble or not would be very useful for event delegation, etc.

@slightlyoff

We need this capability somehow. The real-world perf implications are big enough to justify it.

Passivity alone as a 4th param feels busted. We shouldn't be building new APIs without any eye towards extensibility (particularly if they already take an inscrutable, optional boolean positional param already), so the property bag feels like a good answer.

The current proposal and Rick's 3rd option seem like the best alternatives to me. The current option suffers from potential conflict...but does it really? I mean, this is new code that will be written with the intent that it'll be running on new browsers. The lack of a hard error seems the argument against it, but assuming that it's feature detectable (is it?), then I'm not as worried as @jacobrossi is. If this were a behavior change about existing code, that'd be one thing...but this isn't that thing.

Regarding option 3, this should probably just be element.listen() if we go that route.

@RByers
Member
RByers commented Feb 23, 2016

but assuming that it's feature detectable (is it?)

Yes of course - feature detection is essential, but it unfortunately takes a non-trivial amount of code to implement. Some history in #8. Once we resolve this issue, I'll add a detect to Modernizr to make it easy for people.

@RByers
Member
RByers commented Feb 25, 2016

It sounds like there's some strong support (from Olli and Jacob) for option 2, but opposition from Anne and Philip. If you guys were to reach consensus that 2 is better than what's in the spec currently, I'd be willing to switch (it's uglier but I can live with it). But we need to make a decision soon - we have 3 LGTMs on the intent-to-ship and are blocking shipping on only this debate (will probably slip a release as a result).

If I'm understanding the concern properly, I think the reason this is different than other new APIs is that it's going to be really easy for lazy developers to do the wrong thing and have a pretty subtle (as opposed to catastrophic) failure. What if we (call this option 6) tweaked the API to take an options object created by a factory API taking a dictionary instead of taking a dictionary directly. Eg:

addEventListener(type, handler, Event.options({passive:true}))

This would fail catastrophically on old browsers (just the same as adding any new API with a new name), but is easy to feature detect and have a partial forwards-compat polyfill for:

if (!'options' in Event)
  Event.options=function(dict) { return dict.capture || false; }

This seems a little cleaner to me than requiring a 4th argument (although the code is still slightly more verbose) and I think addresses both forwards and backwards compat concerns. If compat concerns go away in the future, we could change addEventListener to also support taking the dictionary directly.

Thoughts?

@esprehn
esprehn commented Feb 25, 2016

I'd much prefer either doing addEventListener(options) or something like rbyers suggests with a new object type. I'm not a fan of the current proposed syntax which is easy to get wrong and not notice.

@foolip
Collaborator
foolip commented Feb 25, 2016

Doesn't Event.options have precisely the same properties as the EventListenerOptions dictionary, except that it's easier to feature detect?

In order to avoid the problem of code that doesn't bother with feature detection, I think the only options are to not overload the third argument, or to let the default value of capture be true. That is frowned upon because { capture: undefined } then has a surprising behavior.

@foolip
Collaborator
foolip commented Feb 25, 2016

Oh, I see, the main point of Event.options would be to upgrade a subtle problem to an exception.

@foolip
Collaborator
foolip commented Feb 25, 2016

Here's a polyfill for the current solution that seems to work in both Firefox Nightly and Chrome Canary, i.e. with and without support for EventListenerOptions as the third argument:

(function() {
  var supportsOptions = false;
  self.addEventListener('', null, { get capture() { supportsOptions = true; } });
  if (!supportsOptions) {
    var aEL = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, callback, options) {
      var capture = options instanceof Object ? options.capture : options;
      aEL.call(this, type, callback, capture);
    };
  }
})();

Tested with http://software.hixie.ch/utilities/js/live-dom-viewer/saved/3919

Is the problem that this is too ugly, or that we don't trust that web developers will keep their stuff working in older browsers for long enough?

@annevk
Collaborator
annevk commented Feb 25, 2016

FWIW, addEventListener(type, handler, Event.options({passive:true})) seems super ugly to me, we haven't used that pattern elsewhere. Overloading type with a dictionary seems problematic too, if someone relies on stringification.

@phistuck

Overloading type with a dictionary seems problematic too, if someone relies on stringification.

Elaborate, please?

@smaug----
Collaborator

Is the problem that this is too ugly, or that we don't trust that web developers will keep their stuff working in older browsers for long enough?

I'd say both.

@esprehn
esprehn commented Feb 26, 2016

Overloading type so addEventListener takes a single argument seems very safe, passing a single argument to addEventListener doesn't make sense today.

@foolip
Collaborator
foolip commented Feb 26, 2016

Unfortunately that would not fail hard in Safari, where addEventListener({}) does not throw an exception, even though addEventListener.length is 2. I only recently fixed this in Blink, so any Chromium-based browsers using M48 (the current stable version) or earlier are also affected.

@annevk
Collaborator
annevk commented Feb 26, 2016

@esprehn that is a good point. I wonder what @domenic thinks about that since it would require an IDL overload and we're not huge fans of those.

@RByers
Member
RByers commented Feb 26, 2016

@smaug---- / @jacobrossi, would switching to a 1-argument form like this address your concern? It's still not "forwards compatible" or "easy to feature detect" but it does at least fail hard in most browsers (just like any new API). Is that the crux of your concern?

@tabatkins
Collaborator

If we're going to do that, I recommend we go with @slightlyoff's suggestion and name the API .listen(). It's shorter, matches library conventions, and fails on all browsers.

@domenic
Collaborator
domenic commented Feb 26, 2016

If we have to do that, I'd prefer .addEventListener2. A one-argument form is an ugly hack, and not one we want people to use often.

@tabatkins
Collaborator

??? Why is it a hack? It just means that it's entirely named arguments, no positional ones. We can't do both like Python does, so it's always a balance between the two.

@domenic
Collaborator
domenic commented Feb 26, 2016

It's a hack to get around the idea that people aren't going to properly feature-detect .addEventListener(name, function, options).

@domenic
Collaborator
domenic commented Feb 26, 2016

To be clear, I think the existing spec is good. But if we can't accept an options argument replacing the boolean, then we shouldn't even try. Do something like .addEventListenerPassive(name, function, capture) or similar. No overloads with radically different parameters, especially not squatting on good names we might want to use in the future for non-hacks.

@phistuck

Semantically, a single options argument is probably appropriate only when all of the options are optional. In this case, you must have a type and a handler.
Like fetch, for which you must specify a url parameter and an optional options parameter.

While I appreciate the semantical value, a single argument seems clearer and nicer (and modern, or familiar, jQuery like) in some sense.

@jacobrossi

My point is much more basic than this. Passive event listening is nothing more than a perf optimization. The API should allow this optimization while gracefully falling back in other browsers. A new event registration method doesn't have this quality nor does the third argument options dictionary. The proposal to make this the fourth arg enables this simple perf opt easily while preserving compat with today's browsers and those that seek the perf gains in other ways (e.g. we don't feel the urgent need to implement this) .

@smaug----
Collaborator

I agree with jrossi, and I don't see anything horrible with 4 param approach.

!!{} happens to evaluate to true, so it should be safe even in Gecko where 4th param is interpreted as boolean currently, defaulting to true for web content.

@domenic
Collaborator
domenic commented Feb 27, 2016

It's not a perf optimization though. It changes preventDefault and everything that keys off of an event's canceled status. This is a new feature with serious semantic impact on event life cycle.

@jacobrossi

It's only intended to be used in scenarios where the event already wasn't being canceled, so it's not very serious of a change in practice.

@domenic
Collaborator
domenic commented Feb 27, 2016

Maybe, but it's also only intended to be used in scenarios where you feature test its presence.

@jacobrossi

Pretty much all features are intended to be used with feature detection, but the reality is code isn't always written that way. We have the opportunity to avoid an issue here, we should take it.

@RByers
Member
RByers commented Feb 27, 2016

Thanks Jacob, I think I understand your argument better now: Normally when we add a new API we don't get the luxury of forwards compat, but at least feature detection is normally trivial. Here, with the 4th arg design, we could probably get away with treating it as forward compatible, not needing a polyfill. We shouldn't squander that opportunity, especially given the non-triviality of feature detection. Olli / Jacob, does that capture your argument accurately?

So if we went with the 4th arg design, would we bother even to recommend using feature detection in most cases? Probably not right? That simplifies things for developers quite a bit and so probably lowers the barrier to adoption of passive listeners. That's a potentially compelling reason to me to accept an uglier API.

None of this makes it any harder to offer a new / simplified syntax in the future if the benefits of adding an alternate method outweigh the cost. It sounds like there's other reasons we might want to do that in the future, so maybe we shouldn't get too hung up in trying to make an already ugly API a little less ugly?

@jacobrossi

Yep that captures my thoughts well, Rick.

@annevk
Collaborator
annevk commented Feb 27, 2016

I don't think it's acceptable to move to four parameters here. Developers dislike the current API enough as it is, we should not make it worse.

@jacobrossi

What's your acceptance criteria here? One of the utmost concerns of API design for web developers is compatibility, which is the issue being raised here. Getting rid of an historical boolean argument seems like the least of web developers' pains.

@tabatkins
Collaborator

Again, the issue raised here is forward compat; the "utmost concern of API design" is backwards compat, which nobody is raising as a serious issue here.

The boolean argument is nonsense cruft which, luckily, can usually be ignored. This change (and the future changes it enables that will add more options to the dict) will require authors to understand and specify it every single time they want to use one of the further options.

As has been pointed out, feature-testing the 3-arg variant, while not trivial, is possible without too much difficulty, and can be easily built into any of the feature-testing libraries like Modernizr. Or if we absolutely cannot accept this, creating a new method with 3 args (and giving it a nice name like .listen) is acceptable to at least some people in this conversation too, and sounds great to me.

(I'm sorry that adding this useful feature is requiring so much side-argument over syntax.)

@tabatkins
Collaborator

Note as well that all dict-taking APIs have this feature-testing difficulty. Passing an options dict with unused properties doesn't cause an error, so you always need something like what Rick's example shows to tell whether the property in question is being used. This is not a new or unique difficulty that we're dealing with here, so it shouldn't be anything near a deal-breaker.

@jacobrossi

Note as well that all dict-taking APIs have this feature-testing difficulty.

This API is special in that it is attempting to replace an already defined API with different behavior. In the failure case here, it produces bugs that are very expensive and confounding for developers to debug (a change in the event timing). We should help developers (and their/our customers) avoid that pain by designing a better API like proposal 2.

Never have we ever been able to rely on developers' use of feature detection as a replacement for sensible API design.

This feature is a perf optimization with no user observable change in behavior. It's very similar to will-change in that regard. will-change gets ignored by browsers that don't support it and falls back gracefully. This API can and should do the same. Other than "Booleans suck," I haven't seen a technical argument as to why we can't do that for our customers.

For at least several years, hundreds of millions people will be using browsers that will be at risk of compatibility woes caused by this API not being designed properly, and web developers will be the ones that feel the pain of figuring it out and fixing it. That's just not warranted for such a simple perf optimization (not to mention, one for which better APIs without this perf problem already exist).

I'm sorry that adding this useful feature is requiring so much side-argument over syntax.

If it was just minor syntax that was affected, like changing API names, I'd agree. But this discussion has actual interoperability issues at hand. After interop disasters like -webkit APIs, I'm confident developers want us to spend the extra few days to think through API design in an interoperable manner.

@tabatkins
Collaborator

This API is special in that it is attempting to replace an already defined API with different behavior. In the failure case here, it produces bugs that are very expensive and confounding for developers to debug (a change in the event timing). We should help developers (and their/our customers) avoid that pain by designing a better API like proposal 2.

I don't think this is different from any other new option that can cause a significant behavior change.

However, if you feel that strongly about this, then let's just make a new method name. I feel strongly that we should not force authors to pay the boolean-understandability tax for the infinite future, and authors already complain about the length of the addEventListener name. Let's just make a new .listen() method that takes an event name, a callback, and an options arg, defined for now to have two keys: "passive" and "capture".

@dtapuska
Collaborator

I don't think that we should just consider this feature as a perf
optimization. We are effectively designing an alternate approach for
options passed into any event listener. Mozilla's 4th argument for
untrusted events could have just been an option in the dictionary.

My gut tells me that we should just use a new named API like listen(); as
Tab indicates.

On Mon, Feb 29, 2016 at 12:07 PM, Jacob Rossi notifications@github.com
wrote:

Note as well that all dict-taking APIs have this feature-testing
difficulty.

This API is special in that it is attempting to replace an already defined
API with different behavior. In the failure case here, it produces bugs
that are very expensive and confounding for developers to debug (a change
in the event timing). We should help developers (and their/our customers)
avoid that pain by designing a better API like proposal 2.

Never have we ever been able to rely on developers' use of feature
detection as a replacement for sensible API design.

This feature is a perf optimization with no user observable change in
behavior. It's very similar to ''''will-change'''' in that regard.
''''will-change'''' gets ignored by browsers that don't support it and
falls back gracefully. This API can and should do the same. Other than
"Booleans suck," I haven't seen a technical argument as to why we can't do
that for our customers.

For at least several years, hundreds of millions people will be using
browsers that will be at risk of compatibility woes caused by this API not
being designed properly, and web developers will be the ones that feel the
pain of figuring it out and fixing it. That's just not warranted for such a
simple perf optimization (not to mention, one for which better APIs without
this perf problem already exist).

I'm sorry that adding this useful feature is requiring so much
side-argument over syntax.

If it was just minor syntax that was affected, like changing API names,
I'd agree. But this discussion has actual interoperability issues at hand.
After interop disasters like -webkit APIs, I'm confident developers want us
to spend the extra few days to think through API design in an interoperable
manner.


Reply to this email directly or view it on GitHub
#12 (comment)
.

@domenic
Collaborator
domenic commented Feb 29, 2016

I strongly oppose squatting on a good name like listen for the current EventTarget design. People expect us to use that for some kind of redesign, e.g. based on observables or https://gist.github.com/annevk/4475457. If we're going to use up a good name we need to spend some serious time redesigning EventTarget to be more user-friendly, at least, even if we're not going whole-observables.

@annevk
Collaborator
annevk commented Feb 29, 2016

Agreed, we should not make a legacy API worse and we should not use a new name lightly. We've been iterating on this third parameter switch for a while before it was put into the standard. I still don't see a good enough reason to change that. (Overloading the first argument seemed attractive at first, but that breaks with how new Event() arranges its parameters. Not great.)

@phistuck

What about overloading the second parameter?
It already accepts an object with a handleEvent method and uses it as the callback. Seems almost classic...
Or is there a this issue there? I forget.

@jacobrossi

However, if you feel that strongly about this, then let's just make a new method name.

I agree with @domenic. If we're going to create a new method, then we should take the time to do a major overhaul of events. But I don't think we want to gate this perf optimization feature on that redesign. If and when we do that overhaul, then the minor inconvenience of the 4th arg goes away.

We've been iterating on this third parameter switch for a while before it was put into the standard. I still don't see a good enough reason to change that.

When compatibility/interoperability is at stake, the burden of proof is the other way around. You need to show sufficient argument why a breaking change to one of the most popular APIs on the web is necessary for this feature. "We don't like the old boolean argument" is not sufficient.

@phistuck

I just checked - yes, this is the second parameter instead of the event target. Perhaps this is not so bad, though.

@domenic
Collaborator
domenic commented Feb 29, 2016

This is not a breaking change. It's important everyone understand that. It is not forward-compatible, but is backward-compatible.

@jacobrossi

Let's not argue the semantics of a "breaking change." I think we understand what breaks and doesn't here.

@annevk
Collaborator
annevk commented Feb 29, 2016

The web doesn't have a forward-compatible principle.

@RByers
Member
RByers commented Feb 29, 2016

What about overloading the second parameter?

That's discussed (and rejected) in #11.

@jacobrossi

The web doesn't have a forward-compatible principle @jacobrossi.

Says who? Forwards compatibility is important opportunity to take advantage of that benefits our customers, both developers and users.

About 1.2M (of a sample size of ~2.5M) pages use aEL with three arguments. As these pages update to take advantage of this new feature, forwards compatibility serves as an important protection to the hundreds of millions of customers browsing without this feature.

Forwards-compatibility isn't a MUST in all API design cases. But when we have an easy opportunity to enable it and we're talking about one of the most popular DOM APIs, we should take it.

@tabatkins
Collaborator

Let's not argue the semantics of a "breaking change." I think we understand what breaks and doesn't here.

But apparently we don't, since you're using the term "breaking change" to refer to something that is slightly harder to feature-test for than normal. ("normal" meaning "this API will throw an error if used in legacy browsers" or "can be tested for just by seeing if a property exists on some object").

Overloading the third argument of aEL() to allow an object is not a "breaking change" by any reasonable definition of the word. The burden is thus relatively low here - we're simply deciding whether it's more important that (a) the API be slightly easier to feature-test for during the transition period, or (b) the API not enshrine a confusing boolean param in its signature forever.

Worst case, authors just don't use the new API until it's widely supported enough to depend on. This is an unfortunate outcome, but not unusual - the same thing happens regularly for major CSS features. It's also the outcome whenever some API depends on functionality that can't be polyfilled.

But that shouldn't happen, because it's not that hard to feature-test this. It's non-trivial, but the code to do it is short, and Rick already demonstrated it. Building this into the feature-testing libraries like Modernizr will be very easy and cheap. I don't think this is a big pain for authors to pay for the short transition period (and we can make it shorter by all pushing to this syntax form quickly).

@RByers
Member
RByers commented Feb 29, 2016

Let's not argue the semantics of a "breaking change." I think we understand what breaks and doesn't here.

In a way this is exactly what we should be arguing. We all care about the same principle here: most code on the web should behave the same in the browsers that the vast majority of users are using. We just have different opinions on how the different choices here are likely to affect that principle. Ideally we should be trying to bring data to try to back up these opinions.

In particular, I'm still concerned that if we try to use an API form that's forwards compatible it may cause more compat issues than it solves. I'm thinking about the case of touch-action and hammer.js. In that case a developer didn't fully understand the implications of a new API, and so chose a default that appeared to work in his quick tests. Then when site developers found it broke their site on IE, they added UA checks to avoid using the library in IE. Then when Chrome shipped an identical feature to IE, we had thousands of sites that were badly broken only in Chrome.

The similar risk here is that someone cargo-cults the use of passive listeners, tests thoroughly only on browsers without passive listener support, not realizing their logic relies on preventDefault continuing to work from within a passive listener. This can result in some subtle bugs (eg. dragging-while-zoomed being broken). Or in the flip scenario, testing only on browsers with passive listener support, and iterating until things appear to work - not realizing they have some incorrect preventDefault calls that are being ignored. On old browsers scrolling may now be completely broken. The solution for both types of issues, I think, is to strongly encourage the use of a good polyfill. Forward compatibility here is a unicorn - would be nice but doesn't really exist in practice.

@jacobrossi

The similar risk here is that someone cargo-cults the use of passive listeners, tests thoroughly only on browsers without passive listener support

I really doubt anyone would do this nor have I ever seen anyone do this to an API before (evangelize using something without using it themselves?).

Or in the flip scenario, testing only on browsers with passive listener support, and iterating until things appear to work - not realizing they have some incorrect preventDefault calls that are being ignored.

This scenario implies they have listener code that calls preventDefault and then they make that listener passive. If the preventDefault calls were incorrect, then they were already broken in browsers before they made it passive. If the scenario is that they somehow iterate the listener after making it passive to now have incorrect preventDefault calls, then that's a scenario that seems very far fetched (or at least an edge case and not something we'll see en masse) to me.

The cargo-cult scenario we should be anticipating is an evangelism of converting your listeners to passive wherever you can. This will easily get boiled down too simply to "just swap out the third arg for this object and you'll make your touch handlers fast in Chrome!" Devs will flock to implement this "quick tip" without thought of how objects will be considered truthy in other browsers.

This is going to be a much more mainstream scenario than the others you presented IMO. We see this kind of thing all the time at conferences and on blogs where folks put together "10 quick perf tips" (like the translate3d "trick" for GPU accelerating all the things!).

@grorg
grorg commented Mar 3, 2016

/me reluctantly enters this heated (but very polite) discussion 😄

If it helps move forward, I would categorize my + smfr (Simon Fraser) + WebKit's position right now as preferring the 3 argument form over the 4 argument form.

I also like the idea of going towards an API that only takes a single dictionary.

@RByers
Member
RByers commented Mar 8, 2016

Thanks @grorg!

To summarize the current state - the two main options that appear to have the most support are:

  1. Leave things as-is (EventListenerOptions is the 3rd arg). This is seen as cleanest long-term, addressing the existing boolean arg wart, at the cost of foward compat risk when developers fail to feature detect / rely on polyfills. @annevk (spec) @domenic @foolip @tabatkins (blink) @grorg (WebKit) are in favor of this choice.
  2. Move the options to the 4th arg position. This is much more likely to be forwards compatible, but makes common scenarios more verbose / opaque. @jacobrossi (Edge) and @smaug---- (Gecko) are strongly in favor of this approach.

I'm personally willing to ship either of these, but I need to make a decision one way or the other soon. It seems to me this is a judgement call weighing two different things that are hard to quantify. Does anyone have any suggestion of data they could bring to the argument to better quantify the tradeoff? Would a conference call / video chat be likely to help here?

@RByers
Member
RByers commented Mar 9, 2016

Here's another option that may be crazy or may actually get us the best of both worlds. Like #11, but would it be sufficiently compatible if we changed the signature to to addEventListener(DOMString type, EventListenerOptions? options, optional boolean capture) and add required EventCallback handleEvent to EventListenerOptions (with `callback EventCallback = void(Event)') or something to that effect? That would have the advantage of letting people put the options before the handler, as in:

  element.addEventListener("touchstart", {
    passive: true, 
    handleEvent: function(event) {
      ...
   }});

I think this might be both forwards and backwards compatible for ECMAScript bindings at least. We'd have to define what happens if both the 3rd arg and the capture option is specified (3rg arg always wins? throw?), and of course it's not forwards compatible anymore if you specify the capture option without also using the 3rd arg. But maybe those are minor issues relative to the strong concerns some folks have with all the other options?

@RByers
Member
RByers commented Mar 9, 2016

Oh - and you commented that it was rejected. :) #11

Yep, I'm saying we should revisit that and discuss it in more detail. Sorry we didn't do that sooner - I don't think we gave the idea the attention it deserves. In the original approach I think we were thinking of persisting the legacy "callback interface" pattern, but this is a little different (at least conceptually) - just a dictionary that happens to be structured such that (in ECMAScript) it works the same.

The only issue might be the unusual this (unless you use arrow functions, of course).

Yeah for compatibility I think we'd have to say that handleEvent is called with this set to the EventListenerOptions value, which is kinda weird but maybe tolerable if it gives us something all the engines can live with?

@phistuck
phistuck commented Mar 9, 2016

I know of at least one (popular?) library that used (or uses?) that handleEvent pattern - iScroll. So there might be compatibility issues with this approach (what if the object happens to also define a capture or passive property?). An HTTP Archive research would be a place to start, but a use counter would be even better (or an internal Google index search). It might be a bit tricky to find, though.

@smaug----
Collaborator

I haven't seen anyone coming up with any compelling reason to why not use 4th param version here.
The slight more verbosity shouldn't really be a deal breaker here. And if someone doesn't like the
4th param version, "just polyfill" it everywhere and use whatever syntax script library author happens to prefer. But let's not force everyone to polyfill.

If we later want to add some nicer API to add listeners, that should use totally different function name. Designing that API will take significant amount time.

(I kind of like that 2nd param as object approach but unfortunately it doesn't solve the issue.)

@tabatkins
Collaborator

I haven't seen anyone coming up with any compelling reason to why not use 4th param version here.

The fact that "this API shape is terrible, everyone agrees that it uses a terrible pattern, and we shouldn't make it more terrible" isn't a compelling reason is why the DOM has a well-deserved reputation of being terrible. :/

@smaug----
Collaborator

3rd param version isn't really making the API less horrible, so I don't see the point.
In fact, now that I think of it, I think it is making the API even worse, by making this magical
'boolean or object' param.
4th param case has the same issue, but only in Gecko, so that is fine.

@tabatkins
Collaborator

No, I'm referring to API usability. Everyone recognizes and agrees that boolean positional args are terrible API design. The current aEL() suffers from this, but most of the time the boolean arg can be ignored and left off, so it's not too bad.

The 4-arg proposal kills that - if you want to specify any of the extended options (passive, or anything we add in the future), you'll have to write out the boolean third arg, and remember what it does to make sure you used the correct value.

@jacobrossi

I don't personally think it's worth stressing over the Boolean aspect as this is really only going to be used by a subset of use cases for 2 maybe 3 events (touchstart, wheel, and maybe touchmove since the first one is cancelable) and will very likely be replaced altogether in the future with a better listener registration method.

That said, if the Boolean is a deal breaker for some folks then I really like the 2nd arg approach above. It seems like a reasonable design that gets rid of the forced bool and is forwards compatible.

I'm not too worried about the possible collision with an existing passive member of the object. I tried to grab crawler data on handleEvent usage but it's thrown off by a Google Translator lib that doesn't appear to actually use it in runtime. So I've added a UseTracker to this but we won't get meaningful data for a couple weeks.

@foolip
Collaborator
foolip commented Mar 10, 2016

I don't think the 2nd arg approach will work with EventListenerOptions as a dictionary. The argument is now a EventListener callback interface, which is either a function or an object. If it's a (non-callable) object, the handleEvent property lookup is done when invoking the callback. So the handleEvent property can be replaced after registering the event listener, and crazy things like this work in all browsers:

var callback = {};
document.addEventListener('foo', callback);
callback.handleEvent = function(event) { console.log(event) };
document.dispatchEvent(new Event('foo'));

Even modifying the handleEvent property during dispatch works consistently:
http://software.hixie.ch/utilities/js/live-dom-viewer/saved/3980

A proper WebIDL dictionary with a handleEvent member could not preserve this behavior, so the argument would have to be handled entirely in prose, and it would have the odd characteristic that handleEvent can be modified at any time, but the other "dictionary" members could not.

This does not look promising to me, but if someone has a processing model in mind that isn't too crazy, please elaborate.

@RByers
Member
RByers commented Mar 10, 2016

@foolip ugh, thanks - that's nasty. What do you estimate the chances are that essentially nobody actually does that sort of thing in practice, and so it's something we could break? Eg. have you seen an actual example of that in the wild?

@domenic
Collaborator
domenic commented Mar 10, 2016

We had someone file an issue on jsdom for that behavior not working per spec that we needed to fix, I assume because he was using jsdom to scrape/test a real world website. So, that's one site at least.

@annevk
Collaborator
annevk commented Mar 10, 2016

Note also that this will be different for such callbacks. It'll be the object you passed and not the event target. We cannot change that.

@phistuck

@annevk - I noted that earlier, but arrow functions (with transpilers) should take care of that annoyance anyway.

Regarding the presented issue where the handleEvent method is evaluated at the event dispatching time - I am not sure this matters.
I expect most of the uses to be like -

addEventListener("click", {handleEvent: e => {this.value = "foo"}, passive: true});
addEventListener("click", {handleEvent: e => doStuff(e), passive: true});
addEventListener("click", {handleEvent(e) { doStuff(e) }, passive: true});
@RByers
Member
RByers commented Mar 11, 2016

Ok, I can see why this 2 arg option is likely either too breaking or too weird to spec.

I'd like to try to make the forwards compat argument a little more concrete to see if that helps in evaluating tradeoffs. I think the argument rests primarily on the likelihood of the following situation occurring:

  1. User uses a browser over a year old (sounds like this is the biggest risk, we can discuss the evergreen-but-no-ELO-support seperately), AND
  2. User visits a page that has opted in to using ELO for an event type the user generates (eg. touch), AND
  3. That page fails to follow our feature-detection / polyfill guidance, AND
  4. The listener being incorrectly capturing results in user-visible breakage

My gut conservative instinct of the chances of these occurring are (very roughly without much real data to back it up):

  1. 25% (maybe 40% on mobile, 10% on desktop with usage roughly evenly split?)
  2. 30% (eg. 50% of touch listeners on mobile, after a few years of evangelism)
  3. 1% (we'll work with the biggest frameworks/sites here to make sure they do it right, so in terms of page loads I suspect we can probably get lower than this)
  4. 20% (anecdotally most cases I look at don't matter, but I can believe it's not uncommon they will)

This works out to 0.015% of page views. I'm personally OK with that level of medium-term breakage in order to give developers a more convenient API (similar to what we'd be willing to break on blink in order to remove a bad API that had a strictly superior alternative). Anyone want to argue I was an order of magnitude off one way or the other somewhere? Better yet, anyone want to come up with some data to provide more justified values? I wouldn't, for example, be OK with a total value 10x higher than this - if we believed that was the most likely outcome I'd personally weight the compat costs higher than the developer ergonomics.

@foolip
Collaborator
foolip commented Mar 11, 2016

Reminds me of the Drake equation :)

I think it's correct to view this as a medium term risk, as the numbers will change over time. Non-ELO browser share (1) will start out at 100% and slowly converge to 0%. ELO-using page views (2) will start out at 0% and converge to, say, 30-50%. Non-feature-detecting page views (3) has to start out at almost 0% because of (1), and can only increase significantly once (1) has fallen to some tipping point. (4) is likely roughly constant over time.

Once (2) is "close" to its final value, this will be determined by (1) vs. (3). My hunch is that web developers would be slower to remove/forego feature detection than users are to upgrade, because cleanup work doesn't have much of an upside.

@jacobrossi

Non-feature-detecting page views (3) has to start out at almost 0% because of (1), and can only increase significantly once (1) has fallen to some tipping point.

This is not how it typically goes in reality. Like it or not, many developers only test in one browser. So the tipping point is really just once one browser with significant share (e.g. Chrome) ships with the feature (note Chrome already has ELO, but it isn't really adding a new capability until they ship passives).

@RByers
Member
RByers commented Mar 14, 2016

Reminds me of the Drake equation :)

Yeah, I was thinking that too. Like a Drake equation, despite low confidence in the values, I think this formulation can still make it possible to have a meaningful discussion of probabilities.

I think it's correct to view this as a medium term risk, as the numbers will change over time.

Right, I was trying to focus on the time period where the risk is greatest (basically after our evangelism and it's follow-on effects have had the bulk of the impact in changing code, but before EventListenerOptions is widely supported).

Non-feature-detecting page views (3) has to start out at almost 0% because of (1), and can only increase significantly once (1) has fallen to some tipping point.

This is not how it typically goes in reality.

Yeah I agree this could be (eg.) 1% of the pages using ELO right out of the gate. Now I do think there is likely to be some change over time - with the earliest adopters being the most popular libraries/pages and so getting direct support from us (and so hopefully lower than the long-tail of uses which may be updated via cargo-cult communication).

@foolip
Collaborator
foolip commented Mar 15, 2016

As I wrote my previous comment, I tinkered with a spreadsheet of made-up numbers, where my "tipping point" assumption translated to a medium-term peak in accidental capturing. The problem of feature-frozen and older browsers is what I assumed we were all most concerned about, from @jacobrossi in #12 (comment)

But, yes, testing in only one browser is also a problem. I guessed 0.05%, but if it (3) is 1% from day one, that would make "peak accidental capturing" a near-term event. @jacobrossi, is this the concern, and has this actually happened with a mobile-centric feature first launched in Chrome/Opera for Android over the past few years?

@smaug----
Collaborator

This is very different to some random new feature. We're changing an ancient API in a way where new usage can easily accidentally behave differently in browsers not supporting the new syntax (assuming 3rd param approach is taken).

I'm not aware of any similar change been done to commonly used old APIs. So there isn't much we can compare to.

@jacobrossi

I agree with @smaug---- . You can't really do this type of comparison because we're not just adding a new API, we're replacing an old one with different meaning _and _ the replacement can be "misunderstood" by browsers that don't support it. I can't really think of other mobile features that have done this, which is really why I'm very nervous here.

But yes, the concern is feature-frozen browser (e.g. IE11 or FF ESR, both still in support but don't get new features by design), older browsers (IE <11, Android Browser, other variants of Android browsers, many users but will go away in the _long _term), and any browsers that decide for other reasons not to implement this.

@RByers
Member
RByers commented Mar 15, 2016

Ok it sounds like there's nothing else we can do to better quantify the tradeoffs here. At least if we end up shipping with this risk, it sounds like we'll all learn something new about such risks to help us better quantify similar tradeoffs in the future ;-)

We still need to make a decision on what we'll ship in blink ASAP. Anyone else want to go on the record saying they feel there's enough risk here that it should block shipping an implementation according to the current spec? So far we have @jacobrossi (Edge) and @smaug---- (Gecko) strongly opposed to shipping the design as spec'd due to the potential forwards compat risk.

@RByers RByers self-assigned this Mar 15, 2016
@jacobrossi

I think we can at least agree that the forwards compat risk will be non-zero. It would be possible to write a browser extension that wraps AEL and flips the third argument to true. Browsing around with this would give you more data on how much user-facing breakage would occur. Obviously this won't tell you exactly what will break (nothing can tell the future), but it will give you data on the propensity for a change in the event phase to cause user-facing breakage, which would be valuable (#4 in your matrix).

@jacobrossi

I would add I wasn't convinced by the discussion above that the handleEvent approach can't work. I don't see how the mutability of handleEvent is relevant. It doesn't say anything about passive needing to be mutable. All that changes is at what point in the processing steps you read and store the passive value (is it at registration or dispatch). handleEvent can keep having the odd behavior while the new feature doesn't. As pointed out above, the this difference is also minor and has at least two options (arrows or bind) for the few times it's an issue.

@foolip
Collaborator
foolip commented Mar 17, 2016

@jacobrossi, @smaug----, I get that this is a new situation and that we can't say anything for certain, but I'm still curious how you see the risk over time, where "peak accidental capturing" is and what choices, omissions or misunderstanding by web developers is the main driver of that risk. I assume you have some suspicion, however speculative.

As for the handleEvent approach, it's definitely possible to spec and implement, but it'd have to be entirely custom behavior, as the second argument would be neither a dictionary nor a callback interface. It would also leave NodeFilter as the web platform's only remaining callback interface, and the code for that bit of crazy legacy wouldn't be shared with addEventListener's new second argument. (I recently measured to see if NodeFilter could be made a callback function instead, but no can do.)

@RByers
Member
RByers commented Mar 17, 2016

I think we can at least agree that the forwards compat risk will be non-zero.

Absolutely, and I'm now convinced that it's higher than the trivial levels I originally thought from gut instinct (my 0.015% of page views guesstimate is certainly non-trivial).

It would be possible to write a browser extension that wraps AEL and flips the third argument to true. Browsing around with this would give you more data on how much user-facing breakage would occur.

Is your hunch that this is likely to be a lot higher than my guesstimate (eg. much closer to 100% of typical inputs being affected than 20%)? If this turned out to be, say, 80% then I'd certainly be more concerned about the forward compat risk.

I just did a very quick unscientific experiment. I injected this code before load:

var oldael = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, handler, capture) {
  console.log('addEventListener', type); 
  oldael.call(this, type, handler, true);
};

Then I browsed around GMail and Facebook for a few minutes using mouse and keyboard. I didn't notice a single obvious issue. Certainly not a very scientific test, but enough to tell me that something as high as 80% is pretty unlikely. If you want to collect some more scientific data than that, I'd love to hear the results!

@jacobrossi

I'm still curious how you see the risk over time, where "peak accidental capturing" is and what choices, omissions or misunderstanding by web developers is the main driver of that risk. I assume you have some suspicion, however speculative.

No amount of data can predict that future, unfortunately. It is a very reasonable expectation that quantity "# 1" above will be significant for many years. Mozilla, Microsoft, and others have millions of customers with the valid constraints of scenarios like mission critical enterprise applications and complex IT rollout management that necessitate a slower rollout of latest browser versions.

As for the handleEvent approach, it's definitely possible to spec and implement, but it'd have to be entirely custom behavior, as the second argument would be neither a dictionary nor a callback interface.

Probably the most complex thing here is writing the spec text. The implementation is trivial. The cost of spec text authoring shouldn't contribute to our decision here.

Is your hunch that this is likely to be a lot higher than my guesstimate (eg. much closer to 100% of typical inputs being affected than 20%)?

I honestly haven't a clue. 20% sounds as good of a guess as any other without data. :-)

A more interesting scenario that can't easily be tested is the use of a touch event library. If the library updates to opt into passive listening but the hosting page does not, then the relative order of the library's listeners to the page's could be reversed. You can't test this scenario with my suggested methodology. After thinking further, my methodology could often still preserve the relative ordering of handlers as it assumes all listeners become capture listeners.

@RByers
Member
RByers commented Mar 17, 2016

The implementation is trivial. The cost of spec text authoring shouldn't contribute to our decision here.

I agree we shouldn't give the spec text complexity much weight in this debate. But I don't think the implementation is trivial. At least in blink I believe this would require some pretty weird custom bindings code, and we're actively trying to remove custom bindings for maintenance and security reasons. And ensuring all that custom code behaved exactly the same as implementations in other browsers would require some non-trivial testing.

It seems like a tiny servicing patch that accepts a dictionary in the 3rd argument position would be a lot simpler implementation wise (and probably simpler than most security fixes which are presumably still getting into ESR browsers, right?). Not that I'm proposing that as a real solution (eg. we have plenty of Chrome Android users who never connect to Wi-Fi so generally don't get Chrome updates at all, security or otherwise!).

@jacobrossi

I would suspect you must have some sort of specialized binding logic here already given the definition of how handleEvent must work. At least in our codebase, this is just augmenting that. But I trust you understand the Blink event dispatch architecture far better than I.

Our servicing bar for feature-locked browsers isn't based on the size of the code change, and for various reasons as you point out, isn't effective.

@RByers
Member
RByers commented Mar 17, 2016

I would suspect you must have some sort of specialized binding logic

Thinking further, maybe what you're proposing here is not some weird dictionary with special semantics for compat, but just to use/extend the existing callback interface pattern as discussed in #11? @paulirish is also telling me offline he likes that option. Let's re-open #11 to discuss that option in more detail.

@jacobrossi

Ah yep. From the web devs perspective this is basically the same thing. That's how I was approaching it.

In EdgeHTML, this is just a few lines in the existing fUseHandleEventCallback branch of registration to save the mayCancel bit to the listeners table. Then a few lines of code in the preventDefault path to ignore the calls if the bit was set for the listener.

@paulirish
Contributor

I think I'm up to date on this discussion, so I'll wade in.

The 3rd arg proposal

el.addEventListener(evt, fn, { passive: true })

It's true that to use this for real, developers must feature detect it, in order to not accidentally kick some browsers's listeners into capturing phrase. I was originally a bit hesitant about this, as the detect looked quite hairy, but as I rewrote it and discussed with the Modernizr team, I feel comfortable with the detect and think developers would be okay with using it.

I'm also confident that between the documentation (MDN, etc) and general guidance, developers would reliably use the feature detect whenever they adopt the feature. That said, requiring the detect because of capture phase, which very very few developers understand, let alone use, is kind of a bummer.

The 2rd arg proposal

el.addEventListener(evt, { handleEvent: fn, passive: true })

I like the look of this, and TBH handleEvent opens up some opportunities (e.g. easily removing handlers) that most developers are unaware of. But generally this works very well as no detect is required (assuming you're OK with breaking in <= IE8).

this context will now be different, but most developers need to rebind to something else already; so the change is nearly moot.

From a compatibility perspective…

  • With the 3rd arg, if you forget to use the feature detect, it's certainly possible to flip your listener to capture phase in a browser you didn't test on. That's a fairly silent "failure", which will be hard to track down if it's causing problems.
  • With the 2nd arg, it'll be broken in your browser immediately if you've got an error with this. If you've got no problems in your browser, all other browsers will be fine. (And if you serve <=IE8, it'll throw. And that's fixable with a small polyfill)

So, I think the 2nd arg has better ergonomics for both new old browsers. So I'll join the fun over in #11. :)

@RByers
Member
RByers commented Apr 2, 2016

We've discussed this debate some more within Google, and although we still prefer the existing (3rd arg) approach, we agree we could live with the 4th arg one. We respect that Microsoft and Mozilla have had a different experience with web compat than we have, and it's clear that they feel MUCH more strongly about the need to address this concern than we do about the costs. So @jacobrossi and @smaug---- , if you still feel you really can't live with the 3rd arg design, then as a sign of goodwill we (Google) are willing to concede this point. I also spoke with @grorg offline and he agreed ("I think we should just accept it and move on"). Note that our position on forwards compatibility generally has not changed - we are not willing to limit platform evolution out of fear for the behavior of older browsers.

Concretely this change would mean that instead of telling developers that (for the foreseeable future) they must use code like this:

var supportsCaptureOption = false;
addEventListener("test", null, { get capture() { supportsCaptureOption = true; } });

function myAddEventListener(target, type, handler, options) {
  var optionsOrCapture = options;
  if (!supportsCaptureOption)
    optionsOrCapture = options.capture;
  target.addEventListener(type, handler, optionsOrCapture);
}
...
myAddEventListener(window, "touchstart", handler, {passive:true});

They should instead write:

function myAddEventListener(target, type, handler, options) {
  target.addEventListener(type, handler, options.capture, options);
}
...
myAddEventListener(window, "touchstart", handler, {passive:true});

Or if they prefer for one-off uses, instead of:

var supportsPassiveOption = false;
addEventListener("test", null, { get passive() { supportsPassiveOption = true; } });
window.addEventListener("touchstart", handler, supportsPassiveOption ? {passive:true} : false);

Just do the ugly but trivial thing:

window.addEventLilstener("touchstart", handler, false, {passive:true});

The extra arg is clearly a wart, but it really doesn't seem THAT bad to us compared to explicit feature detection.

There is, however, one extra complication here. 0.5% of the top 10k websites already pass a boolean in the 4th arg position. So for web compat, we need to define the API to permit (but ignore) boolean values in the 4th arg position:

void addEventListener(DOMString type, EventListener? callback, optional boolean capture = false, 
                      optional (EventListenerOptions or boolean) options);

dictionary EventListenerOptions {
  boolean passive;
};

This is ugly, but only for the spec / implementors (shouldn't really affect developers), so I can accept it and move on.

@annevk @foolip WDYT?

@smaug----
Collaborator

I haven't seen any convincing attributes why 3rd param approach should be taken.
We're talking about very old very core API here, so making breaking changing shouldn't be done lightly.

And in Gecko we'd anyway need to define the 4th param to be (EventListenerOptions or boolean) so that we can keep supporting the "allowUntrustedEvents" stuff.

A good thing is that unlike 3rd param as boolean, 4th param in Gecko defaults to true for web content. So passing {} as 4th param doesn't change the behavior.

@jacobrossi

I think the 4th arg is a good compromise, and, given the exhaustive discussion we've had on alternatives, it feels like a reasonable approach. I appreciate your willingness to consider these concerns!

@annevk
Collaborator
annevk commented Apr 5, 2016

I still disagree with the four parameter solution for extensibility of addEventListener(). I agree that the transition path for three parameters might be a little bumpy, but we're here designing the fundamentals because we believe the web to exist for several more decades, if not much longer. That at least goes for the WHATWG community which as far back as 2003-2004 revived interest in the foundations and how they should function, and I believe that goes for all of you too.

The future is longer than the past. We should take a little short term pain to gain long term improved developer ergonomics. If we are to ask other, e.g., @dherman or @domenic from TC39, I'm sure they would agree that developer ergonomics is of vital essence to the platform. It's not something to just brush away.

Also, there’s been a number of requests from developers which we can easily add once this is deployed:

  • A way to let a listener clean itself up when invoked once.
  • A way to let a listener only be invoked for AT_TARGET (there's also been requests for only invoked during CAPTURING_PHASE (excluding AT_TARGET), only during BUBBLING_PHASE (again excluding AT_TARGET), and during all phases).
  • A way to list a listener override the bubbling behavior of an event.
  • The previous feature combined with selector-based filtering gives us event delegation.
  • Filtering out untrusted events.

These features will be easier to add if developers build the experience necessary to detect supported dictionary members. And these features will be much easier to get adopted if the API is developer-friendly.

(I'm also not sure why I'm not counted as representing Mozilla or why Mozilla is even listed as an entity in this discussion. Mozillians speak for themselves except for very rare circumstances. So given that Microsoft and Olli are opposed and everyone else in favor, I think there's still rough consensus for the current approach in the DOM Standard.)

@foolip
Collaborator
foolip commented Apr 5, 2016

To point out the obvious, having EventListenerOptions or boolean where the boolean does nothing is a pretty bad API.

And in Gecko we'd anyway need to define the 4th param to be (EventListenerOptions or boolean) so that we can keep supporting the "allowUntrustedEvents" stuff.

@smaug----, do you mean to say that you'd keep this behavior even after you implement EventListenerOptions? The spec will have to say that the boolean does nothing, and so we wouldn't have interop if you keep it. Unless you're fairly certain you can remove it from Gecko, we might be stuck in that state for a long time.

@smaug----
Collaborator

do you mean to say that you'd keep this behavior even after you implement EventListenerOptions?

yes. No plans to remove the 4th param handling before there is some telemetry data about its usage (the interesting data is passing 'false' in web pages.)

At least EventListenerOptions or boolean as 4th param would be a tad less ugly than as 3rd param, since it doesn't make the API crazy, like the spec is now. passing {} as 3rd means non-capturing, but passing !!{} means capturing.

(I'm starting to wonder if we shouldn't touch .addEventListener at all, but do something else to achieve the 'passive' handling. No proposals yet though.)

@RByers
Member
RByers commented Apr 5, 2016

@annevk I think the main difference between our arguments is lifespan. I'm guessing feature detection would be necessary in most cases for 3-5 years (even based just on future usage of IE 11). Based on our previous discussions, it sounded like we were likely to define a new API as the replacement for addEventListener in roughly that timeframe (to address a number of different issues). It now sounds that you think that's unlikely, is that true?

If we're likely to stick with addEventListener as the primary event registration mechanism for the next decade or more then I agree with you that ergonomics outweigh forward compat. But if we're likely to switch anyway, then I think the medium term forward compat concerns dominate. So let's do a little poll to try to judge this:

@smaug--- / @jacobrossi: If we go with the 4th arg approach, are you guys willing to commit to investing some resources over the next couple years to work with Anne on defining a possible replacement for addEventListener that solves this, and other ergonomics issues? Obviously we can't commit to ship something without knowing what it is, but we can agree the developer ergonomics problem is worthy of our investment and so commit the resources necessary to participate in design. I can commit to that for blink.

@annevk / @foolip / @domenic: If we go with the 3rd arg approach, do you feel that the overall addEventListener ergonomics are acceptable enough that we can avoid investing in trying to get consensus on a replacement API for at least the next ~5 years? Obviously we may learn something new that changes the equation here, but can we say that based on what we know now we'll plan for extending via the EventListenerOptions mechanism rather than looking for participation on a rename / more radical redesign? I can also commit to this path for blink.

@annevk
Collaborator
annevk commented Apr 5, 2016

I suspect that any kind of full replacement would be driven by improvements in the JavaScript language and those tend to take a long time. And the first such improvement that is on the horizon, observables, might well be made to work with the existing event system. So I was personally quite happy with the three parameter solution we found since it allows for extensions to the API to be made in the nearer future while slightly improving the ergonomics (over the long term).

@foolip
Collaborator
foolip commented Apr 5, 2016

I haven't been very involved with the brainstorming around an addEventListener replacement, but if nobody knows of a feature request where a single event target and single event type is going to be insufficient, I suppose we'll have just have addEventListener for the foreseeable future?

@annevk, can you elaborate on "A way to list a listener override the bubbling behavior of an event" and how it'd work with EventListenerOptions?

@annevk
Collaborator
annevk commented Apr 5, 2016

Sure, I'm not sure if that brings us far into the weeds, but okay: target.addEventListener(type, callback, { ignoreBubbles: true }). And then we'd adjust event dispatch https://dom.spec.whatwg.org/#dispatching-events to check event's bubbles attribute at a later stage, while invoking the listeners, and ignore it completely if the listener has an "ignore bubbles flag" set. This is an oft-requested feature from the developer community, who would very much like all events to bubble, so they can use event delegation tactics on all of them equally.

@foolip
Collaborator
foolip commented Apr 5, 2016

Thanks, I just wanted to see how it would fit into the addEventListener mold. Is there any feature request you're aware of where a new method would make sense?

@RByers
Member
RByers commented Apr 5, 2016

I think the 4th arg is a good compromise,

@jacobrossi FWIW I think that's a mis-interpretation of this entire debate. It's always been 3rd arg vs. 4th arg - there is sadly apparently no compromise in the middle that satisfies both the developer ergonomics and forwards compat concerns.

I think the only "compromise" would be to agree that there are a number of ergonomics issues with addEventListener and so it's important for us to invest in designing a replacement API for the long-term. Absent that we need to choose one side or the other.

@annevk
Collaborator
annevk commented Apr 5, 2016

@foolip I think the main reason for a new method would be to shorten the amount of characters. E.g., try to go from addEventListener to on, although that of course caries a number of risks on its own. The only other potential reason would be TC39 designing its own event API, but although there have been rumors, that seems to be relatively dormant now. Given that the first does not seem attractive from a compatibility standpoint (and just renaming doesn't seem sufficient justification for a new method), and the second is unlikely to happen, I think our best path forward is evolving addEventListener() further based on the design @RByers put in the DOM Standard.

@RByers
Member
RByers commented Apr 7, 2016

Ok, not hearing any response from @jacobrossi or @smaug---- it sounds like nobody thinks we should be investing long-term in a replacement for addEventListener, so we need to assume that we're designing for the next 10+ years. Given the other use cases for EventListenerOptions, making sure developers can use them easily is definitely worth some effort / risk.

Obviously we're still divided on how to weigh ergonomics vs. forward compat. To summarize:

  • @jacobrossi and @smaug---- strongly prefer the 4th arg approach (forward compat)
  • @annevk and @foolip strongly prefer prefer the 3rd arg approach (ergonomics)
  • several others (@grog, @tabatkins, @paulirish, @domenic, etc.) prefer 3rd arg but say they could live with 4th arg if necessary.
  • Others we've reached out to (including others from Mozilla) say they don't have a strong preference.

Chrome 51 is branching tomorrow. We were prepared to switch to the 4th arg approach this week if this discussion had gone differently, but we're now out of time unless we want to delay by another release (which, given our urgency, we'd need a strong justification for). Therefore we believe the time has come to ship 3rd arg and move on despite the disagreement. I'm sorry we couldn't come to a resolution where everyone was happy - I really do highly value all of your input and collaboration.

So that this long debate is not without some value, I'll commit to circling back here once usage has gotten above ~5% of page views in Chrome to see what we have learned about forwards compat risk. In particular, what fraction of the top sites are using EventListenerOptions with and without the correct feature detection (for my <1% prediction).

@RByers RByers closed this Apr 7, 2016
@annevk
Collaborator
annevk commented Apr 8, 2016

We should probably just add another simple feature to EventListenerOptions so user agents not interested in passive can implement that instead and reduce their risk of eventual forward compat issues. I think once: true might be a good fit for that. Removes the event listener once it has been invoked.

@foolip
Collaborator
foolip commented Apr 8, 2016

That would be a great feature to have, yeah.

@annevk
Collaborator
annevk commented Apr 8, 2016

Since there might still be some lurking controversy I created a PR so folks can object: whatwg/dom#207.

@RByers
Member
RByers commented Apr 8, 2016

I like the idea of the feature! But note we already have the "capture"
option if some UA wants ELO but not passive (like Chrome 49&50).

@annevk
Collaborator
annevk commented Apr 8, 2016

I figured that might not be enough incentive to invest resources, but totally true.

@jacobrossi

Ok, not hearing any response from @jacobrossi or @smaug----

I was travelling...

Chrome 51 is branching tomorrow. We were prepared to switch to the 4th arg approach this week if this discussion had gone differently, but we're now out of time

I want to go on record saying I think this is reckless. We just uncovered another issue with how ELO should behave and there isn't a second experimental implementation of this feature. The performance issues of Touch Events (and wheel) have existed for over 5 years -- it can't suddenly be this critical that days matter.

I figured that might not be enough incentive to invest resources, but totally true.

I don't think any browser has expressed an issue investing resources on this feature. At least for us, code on the web using ELO will be a major incentive for us for interop. Additionally, we want to have the passive listeners feature for performance. The issue is extended support browsers that we can't update no matter how much incentive the API presents for new browser versions.

@RByers
Member
RByers commented Apr 8, 2016

Chrome 51 is branching tomorrow. We were prepared to switch to the 4th arg approach this week if this discussion had gone differently, but we're now out of time

I want to go on record saying I think this is reckless. We just uncovered another issue with how ELO should behave and there isn't a second experimental implementation of this feature. The performance issues of Touch Events (and wheel) have existed for over 5 years -- it can't suddenly be this critical that days matter.

Noted.

Days have added up to well over a year debating this API, and 7 weeks actively debating this point. There is rough consensus in the community that shipping this is a good thing to do, and we're well past the point of diminishing returns on further debate. Of course if something new and major comes up before Chrome 51 reaches stable (~May 31) then we can still pull it back. But the cost of slipping another release is substantial, so we'd need a correspondingly substantial potential benefit to do so.

@phistuck
phistuck commented Apr 8, 2016

The issue is extended support browsers that we can't update no matter how much incentive the API presents for new browser versions.

Well... Internet Explorer 11 gets non security fixes on a regular basis, so for that matter - you can update, but you prefer not to update it with this kind of fixes (understandable, of course).

@jacobrossi

you prefer not to update it with this kind of fixes (understandable, of course).

Not really a preference, but a customer promise to the users of these browsers. It defeats the purpose of their existence if you add features like this.

Of course if something new and major comes up before Chrome 51 reaches stable (~May 31) then we can still pull it back.

It would seem to me that #27 is new and major and should have been sorted out before ELO was ever shipped in a browser. It's exactly the kind of thing where a 2nd experimental implementation uncovers an assumption that the 1st implementation didn't consider into the spec. That's why we generally try to do it that way. :-(

EDIT: which is what puts us in this situation which is harmful for the web:

We're actively getting sites to use EventListenerOptions now, so there's some risk we could start to have compat concerns if we don't act quickly.

@phistuck
phistuck commented Apr 8, 2016

Not really a preference, but a customer promise to the users of these browsers. It defeats the purpose of their existence if you add features like this.

The feature to which I was referring here is not to treat an object as true (that would fix the forward compatibility issue). Supporting {capture: true} (or anything else) is a big feature that I would not expect you to backport, of course.

Just a thought.

@annevk
Collaborator
annevk commented Apr 8, 2016

@jacobrossi I think you're overstating things. #27 is pretty minor given the conclusions in the end.

@smaug----
Collaborator

How is @jacobrossi overstating. We're effectively about to make changes to some very core DOM APIs, and realize even before shipping that some of the changes may not be stable yet. And still plan to ship. Doesn't sound too good.

@annevk
Collaborator
annevk commented Apr 8, 2016

Because the unstable bit is pretty close to dead code in practice.

@RByers
Member
RByers commented Apr 8, 2016

If you guys think a second implementation is critical to getting this right, and this API is critical to get right, why hasn't anyone else written a line of code for this yet despite the last 2+ years of Google pushing on this problem? Sorry, but I really don't have any sympathy for "we need two implementations before any one ships" when we've been the only ones investing engineering resources for years.

If you recall from our discussions at the time, I implemented the scroll-blocks-on proposed solution 18 months ago, and have since ripped the whole thing out in favor of this better design based on the API feedback we got. So this is not a case of recklessly shipping the first solution that came to us (despite substantial pressure to just ship scroll-blocks-on).

@phistuck
phistuck commented Apr 8, 2016

If you guys think a second implementation is critical to getting this right, and this API is critical to get right, why hasn't anyone else written a line of code for this yet despite the last 2+ years of Google pushing on this problem?

If I recall correctly, the problem does not exist in pointer events (for touch) and pointer events are being pushed, which is an incentive not to tackle the TouchEvents problem. But I may be misremembering and this whole comment is moot.

@RByers
Member
RByers commented Apr 8, 2016

If I recall correctly, the problem does not exist in pointer events (for touch) and pointer events are being pushed, which is an incentive not to tackle the TouchEvents problem. But I may be misremembering and this whole comment is moot.

Yes, that is correct (for the touch case, wheel is still an issue). I don't personally believe that we'll be able to get all the important touch event code on the web replaced with pointer events.

@FremyCompany

Just for the record, I think the code needed to feature-detect this -- while clever and interesting -- is long, ugly, and very very very very unlikely to be understood my most web developers. There is absolutely no doubt this is going to cause issues in the future to older browsers, and as noted debugging those issues is going to be very very difficult, so most developers will likely give up on web compat at this point.

This change is adverse to the web platform. Some people have older devices which they cannot update to a more modern browser, and this change is going to break them in ways that are not necessary. Saving 4 chars when calling a function is clearly not worth it.

Both the 4th and 2nd argument approach are way better and I wonder why it hasn't been chosen as the path forwards already. By the way, shipping this feature is against the Blink policies which state that:

If a change has high interoperability risk but is expected to significantly move the web forward, Blink will sometimes welcome it after careful, publicly-explained consideration. In such cases, the implementer is expected to: ◦Take on an active commitment to shepherd the feature through the standards process, accepting the burden of possible API changes.

If people from both Mozilla and Microsoft came up with concerns about the feature and asked for updating the API, I am pretty confident the right move was to change the API.

@tabatkins
Collaborator

We've been over the 2-arg approach in #11. It's dead.

As far as we can tell, 3-arg is web-compatible. We'll revert if actual usage proves us wrong.

The "testing is too hard" argument is actually a fully-generic argument against ever adding any behavior-changing options to a dict argument - the same testing will be required before the author can be sure it's safe to use the new option. We can't accept this argument, as it would rule out vast swathes of API design space we've all intentionally angled ourselves toward. If we want to fix this, let's design something that'll work in a reliable way (like how @supports works without needing explicit support).

"Saving 4 chars is not worth it" is also a fully generic argument against ever fixing APIs with terrible ergonomics. It wins a lot, and that's fine, but it's still something to weigh, not something to be automatically accepted.

If people from both Mozilla and Microsoft came up with concerns about the feature and asked for updating the API, I am pretty confident the right move was to change the API.

The concern in question (#27) has nothing to do with 3- vs 4-arg; it's an issue to resolve about any new addition to addEventListener().

@RByers
Member
RByers commented Apr 8, 2016

Just for the record, I think the code needed to feature-detect this -- while clever and interesting -- is long, ugly, and very very very very unlikely to be understood my most web developers. There is absolutely no doubt this is going to cause issues in the future to older browsers, and as noted debugging those issues is going to be very very difficult, so most developers will likely give up on web compat at this point.

If we want to fix this, let's design something that'll work in a reliable way (like how @supports works without needing explicit support).

Agreed having a better pattern for feature detection is worth some more thought. Filed #31.

@FremyCompany

@tabatkins I disagree with you. You can use new dict entries like "capture" in any browser; if the browser does not understand it, it doesn't get the perf benefit, but that is not an issue.

In this case, you make older browsers misunderstand grossly the developer' intentions, by having them understand your dictionary as a Boolean, with the value "true" having a vastly different and often undesirable behavior that is not even a default in your dictionary. That is not web friendly at all.

To save a few chars for when you need a perf improvement in your browser, you will cause older browsers to break miserably by changing the semantics of the API under their feet. The trade-of balance is largely tipping towards "bad idea" in this case.

@tabatkins
Collaborator

@tabatkins I disagree with you. You can use new dict entries like "capture" in any browser; if the browser does not understand it, it doesn't get the perf benefit, but that is not an issue.

In this case, you make older browsers misunderstand grossly the developer' intentions, by having them understand your dictionary as a Boolean, with the value "true" having a vastly different and often undesirable behavior that is not even a default in your dictionary. That is not web friendly at all.

Incorrect - adding new arguments is only "safe" in the relatively rare cases where the new argument just turns on some browser optimizations or similar. "passive" happens to do that, but other arguments that are on the list to possibly add are definitely not like that - for example, limiting the phase you see the event in, or setting up "once-only" listeners. Most new dictionary args are exactly as dangerous to use naively as adding a new positional arg.

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