Closed RByers closed 8 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.
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).
There's renewed debate on this point, let's continue the discussion here?
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":
addEventListener(type, handler, useCapture, usePassive)
. Not compatible with Gecko's non-standard extension, but perhaps that could be broken?passive
the only option for now: addEventListener(type, handler, useCapture, options)
. Still potentially a compat issue for Gecko?addListener(type, handler, options)
. Isn't really any more "forwards compatible".addEventListener(type, handler, useCapture, mustBeFalse, options)
@smaug---- @jacobrossi @domenic @annevk @dtapuska @foolip thoughts?
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.
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?
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.
@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.
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.
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
).
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?
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})
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.
@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.
1/2 are both rather terrible. I really don't see how this case is different from any other API extensions.
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.
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.
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.
We don't address forward compatibility problems. We never have.
And even if we were to start now, preserving that boolean argument is not going to fly.
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?
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
@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.
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---- 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?
Yeah, we've had all kinds of ideas: https://gist.github.com/annevk/4475457.
And the use case is?
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.
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.
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.
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?
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.
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.
Oh, I see, the main point of Event.options
would be to upgrade a subtle problem to an exception.
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?
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.
Overloading type with a dictionary seems problematic too, if someone relies on stringification.
Elaborate, please?
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.
Overloading type so addEventListener takes a single argument seems very safe, passing a single argument to addEventListener doesn't make sense today.
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.
@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.
@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?
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.
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.
??? 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.
It's a hack to get around the idea that people aren't going to properly feature-detect .addEventListener(name, function, options)
.
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.
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.
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) .
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.
Is the requirement to use feature detection or a polyfill OK?
Olli Pettay suggests:
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