WICG / EventListenerOptions

An extension to the DOM event pattern to allow authors to disable support for preventDefault
Other
1.17k stars 140 forks source link

Should the API be forwards compatible? #12

Closed RByers closed 8 years ago

RByers commented 9 years ago

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 commented 9 years ago

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 commented 8 years ago

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 commented 8 years ago

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

RByers commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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

jacobrossi commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

annevk commented 8 years ago

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

foolip commented 8 years ago

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---- commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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---- commented 8 years ago

@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 commented 8 years ago

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

smaug---- commented 8 years ago

And the use case is?

annevk commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

foolip commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

Elaborate, please?

smaug---- commented 8 years ago

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 commented 8 years ago

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

foolip commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

??? 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 commented 8 years ago

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

domenic commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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---- commented 8 years ago

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.