Need pattern for feature detecting dictionary members
RByers opened this issue ยท 87 comments
Many new APIs (and some new arguments to existing APIs) are relying on dictionaries. But doing feature detection of such members requires ugly and complex code like:
var supportsCaptureOption = false;
try {
addEventListener("test", null, Object.defineProperty({}, 'capture', {get: function () {
supportsCaptureOption = true;
}}));
} catch(e) {}
This increases the concern that new APIs will lead to sites being broken on older browsers because developers didn't understand or couldn't be bothered with the difficult feature detection.
In WICG/EventListenerOptions#31 @tabatkins proposed a mechanism whereby all dictionary types would automatically get a JS-exposed object with a property-per-member to enable consistent and easy feature detection.
Thoughts?
I'm a little scared of this idea since dictionaries right now are not "reified" in any way. Their names are for spec purposes only, and can be changed at will. They just represent normal JavaScript objects that authors pass in. Having there be a property on the global for each dictionary, which is going to be some type of... object? array? of supported property keys (if object, what is their value?) is pretty weird.
I don't really have any other great solution though. Something like window.dictionarySupports("EventListenerOptions", "passive")
isn't great either.
(None of this would be necessary if JS hadn't punted on the named-arguments thing. If we had named arguments like Python, this would all be way easier - methods would just throw if you passed a new argument in an old browser, like they do for new positional arguments today. Ugh.)
To be specific, if you define a dictionary like:
dictionary InterfaceMethodOptions {
DOMString foo = "";
long long bar = 0;
}
This would define an InterfaceMethodOptions
value on the global shaped like:
window.InterfaceMethodOptions = {foo: true, bar: true};
By making this happen automatically in IDL, we get reasonably dependable support, without needing to rely on impls (a) remembering to update their "is this supported?" code, and (b) not lying. This is similar to how @supports
works "automatically" by just basing itself on whether the value parses or not.
Can you say why that's better than one of
window.EventListenerOptions = new Set("foo", "bar");
window.EventListenerOptions = ["foo", "bar"];
?
We would definitely need to audit dictionary names in the platform to make sure that none of them have names that are likely to collide with real-world stuff.... Past that this is probably OK, but I agree it should not be raw object. And the reason it shouldn't be is that we don't want to things to go sideways if a dictionary has a "toString" member or whatnot. So a Set
is probably a better idea.
Sure, I don't have strong opinions on the exact shape. A Set works for me.
FWIW we had this exact problem with getUserMedia(constraints)
. We ended up defining navigator.mediaDevices.getSupportedConstraints()
. The implementation was quite trivial. :)
Still, that seems like a lot of bloat on the window.
I suppose we would only need this for dictionaries that are taken as inputs? Some specs have lots of dictionaries that are only ever returned to content.
I kind of prefer the dictionarySupports()
version over defining lots of additional properties on the global. With a method we could also potentially extend it in the future so you can check whether your particular value is supported for a member or not.
Just so long as it's something that can reasonably be done automagically by our IDL handlers.
Yeah, it would totally be IDL-driven. We will probably need to start annotating dictionaries with something akin to Exposed. Otherwise IDL code will need to find where the dictionary is used in order to know what global it's supported on (maybe that's okay?).
Another thing is that we should indeed probably not do this for dictionaries that are solely returned. It only makes sense for those accepted as an argument somewhere. That probably requires an annotation or again some finding "magic".
Why don't we start by adding an extended-attribute that opts dictionaries into this behavior? We can try that out in a few cases, then revisit if/how to change the default.
That probably requires an annotation or again some finding "magic".
You can't do this without annotations in general: there are some APIs that take object
and examine something on it to decide which dictionary type to convert it to...
At the risk of sounding spoiled, I think I would expect the webidl compiler to do this automatically whenever it can (which should be most of the time?), and instead require annotation when using dictionaries in unobvious ways. I worry most spec writers would forget otherwise.
Yeah, I don't see why return-only dictionaries are a problem here. It's not useful to feature-detect them (probably, tho I could imagine some cases), but if leaving them out means we have to affirmatively annotate dictionaries we want in, it's not worth the trouble - we should just put them all in.
I'm fine with the dictionaries using the same [Global]
defaulting that interfaces use.
FWIW, this problem applies to enum values in addition to dictionary members.
Good point. Do enums live in the same or different namespace from dictionaries?
Same namespace, because when you use it as an arg type, you just use the name.
Ah, right. I... should probably address that in Bikeshed. (It treats all the name-definers as separate namespaces right now and won't warn you if names collide.)
Isn't enum arg detection straightforward? obj.foo = 'bar'; obj.foo == 'bar'
Not if the only use of the enum is in a method argument (same as the dictionary issues we're discussing).
Do we want to allow enumeration of supported dictionary/enumeration members? I can see arguments either way, without a compelling use case I'd probably prefer we keep this scoped to feature detection.
The other thing I was wondering about is if we are going to expose dictionaries, should we attempt at normalizing their names somehow?
I think we should do enums separately by the way. They are either ignored (setters) or the method throws, which makes them reasonably detectable. Apart from that, they would require a different API.
And yes, agreed that we should keep it simple initially.
Do we want to allow enumeration of supported dictionary/enumeration members? I can see arguments either way, without a compelling use case I'd probably prefer we keep this scoped to feature detection.
I'm confused - enumerating the supported dictionary members is literally the request here. (Or at least, being able to ask if a given name is a supported dictionary member.) I'm 100% against anything attempting to be smarter such that it can no longer be trivially automated with no human intervention required.
I think we should do enums separately by the way. They are either ignored (setters) or the method throws, which makes them reasonably detectable. Apart from that, they would require a different API.
Why would they require a different API? Afaict they'd have the identical "set of supported names for this type" API.
@tabatkins well, e.g., do enums and dictionary share a namespace? It's also not clear to me why they would be the same API, since you can do much more with dictionaries than simple member checking going forward as I hinted earlier (e.g., seeing whether a member accepts a particular value).
I'm confused - enumerating the supported dictionary members is literally the request here. (Or at least, being able to ask if a given name is a supported dictionary member.)
Right. There have been two main classes of APIs discussed:
dictionarySupports("EventListenerOptions", "passive")
- provides feature detection without enumeration"passive" in EventListenerOptions
- provides both feature detection and enumeration
My question was just whether we considered supporting enumeration in addition to feature detection a good or bad thing. I can certainly imagine cases where allowing enumeration causes more problems than benefits. If we don't have any good reason to want to support it, then we should probably prefer the 1) style over the 2) style as a result.
well, e.g., do enums and dictionary share a namespace?
Yes, this was asked by me and answered by bz immediately prior to your comment: #107 (comment) Everything that can be used as an argument type shares a namespace: interfaces, dictionaries, enums, and typedefs. I opened a Bikeshed bug to enforce that more thoroughly.
Does this issue address or cover the same use cases as https://www.w3.org/Bugs/Public/show_bug.cgi?id=19936? If so, should we close it with a reference to this issue?
@ddorwin I think the motivation for that bug is different. That is about treating invalid values as another value. This is about testing whether a value is supported.
Note that if we don't agree on something here, we're likely to add but-yet-another one-off API in another special case.
We are also thinking about this for Web Share to add a way to feature-detect whether the API supports particular input fields. Right now we are thinking we'll have to convert the dictionary to an interface in order to allow the JavaScript to detect fields.
I had an idea today that I think might actually work. The main sticking point for me is how to avoid exposing the dictionary names to the web; they are currently unobservable, and IMO that is a nice property.
What if we made it easy for dictionary-accepting APIs to get a feature-detection method added to them? I am thinking the following JS code:
createImageBitmap.supports("imageOrientation", "none")
navigator.share.supports("image");
These could be generated by Web IDL such as
interface WindowOrWorkerGlobalScope {
[WithDictionarySupports] Promise<ImageBitmap> createImageBitmap(ImageBitmapSource image, optional ImageBitmapOptions options);
}
partial interface Navigator {
[SecureContext, WithDictionarySupports] Promise<void> share(ShareData data);
};
The limitation of this is that it assumes a single dictionary argument. Hopefully that is all we'll ever need... You could imagine generalizations that allow more, but they get uglier to use.
I like it!
In your "imageOrientation","none"
example that's explicitly testing both that the dictionary member exists, and that the value is a valid member of the enum? Does supports
also handle just checking enum members without any dictionary at all?
Would passing a dictionary possibly work better than strings? Eg.
createImageBitmap.supports({imageOrientation: "none"})
Then in some cases you may just use the same dictionary (or a subset of it) that you'd end up passing to the actual call.
In your "imageOrientation","none" example that's explicitly testing both that the dictionary member exists, and that the value is a valid member of the enum?
Yeah. Off the cuff, the one-argument version is useful for scenarios where the value space is either very specific (e.g. always a Blob) or very general (any
, DOMString
, etc.). While the two-argument form is useful for a limited value space. The clearest example of a limited value space is enum; in the enum case a supports() method could be code-generated with no additional information (either in spec prose or implementations).
Does supports also handle just checking enum members without any dictionary at all?
I suppose it could, although it makes the "only operates on one argument" issue worse. (I.e. now it doesn't work for methods which take one enumeration argument and one dictionary argument.) I think I'd stick with dictionaries for now? Unless we have a list of platform APIs that need this, in which case we could make a more informed decision.
Would passing a dictionary possibly work better than strings?
Hmm, yeah, this might be better. It is more complicated; e.g. now we have to define what happens with partial support (presumably returns false), or the empty dictionary (presumably returns true). And I'm not sure about how you'd use it for web share; would it be navigator.share.supports({ image: new Blob() })
? We should discuss further...
@domenic: Yeah I find it aesthetically much nicer to ask a feature-detect question of the API not the dictionary object. I would rather avoid polluting the global namespace with dictionary names just for this purpose.
On navigator.share
I was leaning more towards adding a separate canShare
method rather than expose the dictionary. Having a standard supports
"sub-method" would help avoid polluting the namespace with canX
methods.
The limitation of this is that it assumes a single dictionary argument.
If we're going with this approach, I don't see why we would bother tying it to the dictionary at all, or restricting it to dictionary-taking methods. Why not just say certain methods have a "supports
" sub-method that takes a single string and returns a Boolean indicating whether that feature is supported. I'm not sure why this "imageOrientation" example has a second argument (what "none" means).
This could be formalized in IDL like this (strawman syntax):
partial interface Navigator {
[SecureContext, SupportsFeatures<"image", "video">] Promise<void> share(ShareData data);
};
The presence of SupportsFeatures indicates that share.supports
exists, and the feature list inside shows what strings are valid arguments to the supports method.
@RByers suggestion works for me also: this generalizes to "the supports
method takes exactly the same arguments as the base method, has no side-effect, and returns true
if it notionally would accept that argument". For example, you could write:
canvas.toBlob(blob => {
let sharedata = {title: 'My painting', image: blob};
if (!navigator.share.supports(sharedata)) {
// error
}
navigator.share(sharedata);
}
So you just call supports
with the same argument you would then pass to the API. The downside of this is: how do we communicate "partial support"; for example, a UA that doesn't support sharing images won't fail if it gets a ShareData with a title and image, it'll just share the title. So should navigator.share.supports
on a non-image-supporting UA return false
if given a title and image, even though it would still partially succeed? Perhaps it's best if I can directly ask the API "do you support image sharing"?
If we're going with this approach, I don't see why we would bother tying it to the dictionary at all, or restricting it to dictionary-taking methods.
Well, part of the idea of this thread is to have it be an easy drop-in for the spec and for implementations so that you can code-generate the method. Indeed the original idea was to auto-generate this for all dictionaries (and enums?) on the platform. My version requires some opt-in per dictionary/dictionary-accepting method, but still code-generates the supports() logic at least.
You seem to be suggesting something that requires more work for spec authors, which is OK. However it also has the downside of possibly being more inconsistent. E.g. it seems like your version of web share doesn't say yes to supports("url")
, which seems bad. In other words it sounds like supports("x")
will only return true for "x"
s added to the spec after some point in time at which the spec authors started caring about feature detection. I think that is not great, compared to automatically synchronizing it with all the keys of a dictionary.
@RByers suggestion works for me also: this generalizes to "the supports method takes exactly the same arguments as the base method, has no side-effect, and returns true if it notionally would accept that argument".
Ah, that helps me understand it better. In particular you could just pass the blob you're planning to pass to share()
anyway.
The biggest issue I see is the one you point out about partial support. In particular I can imagine unfortunate coding patterns which end up attempting all combinations of arguments to find the one that has the maximal amount of features the developer wants to use, but still returns true.
The other issue is "no side effects". Web IDL type coercions will cause side effects; indeed you'll perform any type coercions twice. But in most cases, e.g. those not involving getters or one-shot iterators, the type conversion is idempotent...
it seems like your version of web share doesn't say yes to
supports("url")
, which seems bad. In other words it sounds likesupports("x")
will only return true for "x"s added to the spec after some point in time at which the spec authors started caring about feature detection.
That's valid. I guess you could either add all the dictionary members (but then your IDL is going to have a redundancy where every dictionary key is also listed in the SupportsFeatures
bit), or you could say there are some basic things that any implementation really needs to have to be compliant (for navigator.share
, that's title
, text
and url
) and then there are some "add-on" features that you can omit.
Ah, that helps me understand it better.
Note I am not trying to put words in @RByers mouth; he may have not been thinking of literally passing the full dictionary.
Yeah the partial support issue feels like a deal-breaker, at least for Web Share.
Web IDL type coercions will cause side effects
Ouch, if that can be arbitrary side effects then I'd rather avoid it.
Ouch, if that can be arbitrary side effects then I'd rather avoid it.
Yeah, consider
const shareOptions = {
get title() {
console.log("foo");
return "title";
}
};
if (navigator.share.supports(shareOptions)) { // will log
navigator.share(shareOptions); // will log again
}
I like the supports() proposal. Here is another idea I would just like to throw into the ring...
The fact that unsupported dictionary members are silently ignored is generally nice for compatibility... Until we encounter an option that the web app can't live without. Basically, there are options that fall into the 'like to have' category and others that are required by the web app. Right now, everything is treated as like-to-have. What if we added some form of notation to indicate options that are strictly required. I'm not sure how exactly we'd do this. Here's a straw man proposal: what if a '+' prefix in front of a dictionary key indicated a required option.
Then we could do something like this:
createImageBimap(myBlob, { +imageOrientation: "flipY", colorSpaceConversion: "none" }).then(onLoad, callFlipYPolyfill);
Basically, the call would be forced to fail (in this case reject the promise) when a required option is not supported. Obviously, there would have to be a way to feature detect the special notation.
@junov interesting, although perhaps a separate issue?
In the interest of scoping this proposal, maybe the first version should be a single-arg supports (no enum detection), applicable to methods with one (possibly optional) dictionary argument.
I think the biggest help we could get for moving this forward is a list of APIs that would use it. If we have a reasonable list, we can validate my proposal against it, and compare with @RByers's dictionary-accepting version, and then proceed to spec. Could others help assemble that?
For createImageBitmap() and HTMLCanvasElement.getContext(), enum detection is rather important. See my comment here: whatwg/html#2610 (comment)
Could you clarify a bit what would be the semantics of <class>.supports()
? Does it just mean "it's on my IDL", i.e. no actual support is necessary/guaranteed? Do you think it would be confusing to do:
const myOptions = {...};
if (whatever.support(myOptions)) {
let a = whatever(myOptions);
}
and then a
fails to be created? Or worse, if myOptions
are actually ignored when creating it?
And if the meaning of it is "actually supported", it means it should never be ignored? (If we are looking at this as a feature detection). Otherwise, we wouldn't be really addressing it and supports
would be more of a necessary but sufficient condition? And if so, then I'm not sure I get the utility of this outside of another unreliable feature detection scheme.
My feeling is that we seem to be trying to solve an issue of required/optional parameters, which the "have an options dictionary argument" created.
@fserb: I think that in the cases where there are discrepancies between the implementation's IDL and what is actually supported, this can be simply handled in the implementation by overriding auto-generated supports() with a custom implementation of supports(). I think these cases would be quite rare (options only supported under specific conditions?).
@junov: So you are saying that a feature that is supports()
ed, shouldn't be ignored, right? But then, what's the advantage of this over if (!createImageBitmap(..., {myCrazyOption:}))
?
I didn't suggest the full "pass the exact same argument list" design because I do think the semantics are different. We don't want to imply that the method is doing some arbitrary argument validation. It's simply testing whether the dictionary (and enum) members are recognized or not. I'm also fine with @domenic's simple 2-string-arg version if that's easier to get consensus on.
In terms of where this would be used, here's a specific list that I can keep updated of the specific APIs we know would benefit (and I'll update this comment if more are mentioned).
Interface | method | dictionary | notes |
---|---|---|---|
EventTarget | addEventListener | AddEventListenerOptions | started this debate |
Navigator | share | ShareData | |
ImageBitmapFactories | createImageBitmap | ImageBitmapOptions | needs enum values too |
HTMLCanvasElement | getContext | CanvasContextCreationAttributes | |
MediaStreamTrack | applyConstraints | MediaTrackConstraints | supercedes getSupportedConstraints |
But I don't know how to find the exhaustive set of cases. I guess it ultimately comes down to how often we extend a dictionary or add new dictionary-taking overloads in non-optional ways. I can't think of any automated way to find those cases.
But in case it helps, here's a list of other `dictionary` types currently in blink (not necessarily all used by shipping APIs), excluding ones that I think probably wouldn't benefit (eg. initializer dictionaries - where you can just feature detect the type of object being initialized). For many of these, if we ever have or want to in the future extend the dictionary, `supports` could be valuable.
AnalyserOptions
AndroidPayMethodData
AndroidPayTokenization
AnimationEffectTimingProperties
AssignedNodesOptions
AudioBufferOptions
AudioBufferSourceOptions
AudioConfiguration
AudioContextOptions
AudioNodeOptions
AudioTimestamp
AuthenticationAssertionOptions
AuthenticationClientData
AuthenticationExtensions
BackgroundFetchOptions
BasicCardRequest
BlobPropertyBag
CacheQueryOptions
CanvasContextCreationAttributes
CanvasRenderingContext2DSettings
ClientQueryOptions
ComputedTimingProperties
ConstantSourceOptions
ConstrainBooleanParameters
ConstrainDOMStringParameters
ConstrainDoubleRange
ConstrainLongRange
ConstrainPoint2DParameters
CredentialCreationOptions
CredentialData
CredentialRequestOptions
CSSCalcDictionary
ElementCreationOptions
ElementDefinitionOptions
ElementRegistrationOptions
FaceDetectorOptions
FederatedCredentialRequestOptions
FilePropertyBag
FileSystemFlags
FontFaceDescriptors
ForeignFetchOptions
ForeignFetchResponse
FormDataOptions
GetNotificationOptions
GetRootNodeOptions
HitRegionOptions
IconDefinition
IDBIndexParameters
IDBObjectStoreParameters
IdleRequestOptions
ImageDataColorSettings
ImageEncodeOptions
InternalDictionaryDerived
InternalDictionaryDerivedDerived
KeyframeAnimationOptions
KeyframeEffectOptions
Landmark
LongRange
MediaConfiguration
MediaDecodingConfiguration
MediaElementAudioSourceOptions
MediaEncodingConfiguration
MediaImage
MediaKeySystemConfiguration
MediaKeySystemMediaCapability
MediaRecorderOptions
MediaStreamAudioSourceOptions
MediaStreamConstraints
MediaTrackCapabilities
MediaTrackConstraints
MediaTrackConstraintSet
MediaTrackSettings
MediaTrackSupportedConstraints
MIDIOptions
MidiPermissionDescriptor
NavigationPreloadState
NFCMessage
NFCPushOptions
NFCRecord
NFCWatchOptions
NotificationAction
NotificationOptions
PannerOptions
PasswordCredentialData
PaymentAppRequest
PaymentAppResponse
PaymentCurrencyAmount
PaymentDetailsBase
PaymentDetailsModifier
PaymentDetailsUpdate
PaymentInstrument
PaymentItem
PaymentMethodData
PaymentOptions
PaymentShippingOption
PerformanceObserverInit
PeriodicWaveConstraints
PeriodicWaveOptions
PermissionDescriptor
PhotoSettings
Point2D
PositionOptions
PropertyDescriptor
PushPermissionDescriptor
RegistrationOptions
RelyingPartyAccount
RequestDeviceOptions
RTCAnswerOptions
RTCConfiguration
RTCIceServer
RTCOfferAnswerOptions
RTCOfferOptions
ScopedCredentialDescriptor
ScopedCredentialOptions
ScopedCredentialParameters
ScrollOptions
ScrollToOptions
SensorOptions
StorageEstimate
TextDecodeOptions
TextDecoderOptions
USBControlTransferParameters
USBDeviceFilter
USBDeviceRequestOptions
VideoConfiguration
VRLayer
WebGLContextAttributes
WorkletOptions
The biggest issue I see is the one you point out about partial support. In particular I can imagine unfortunate coding patterns which end up attempting all combinations of arguments to find the one that has the maximal amount of features the developer wants to use, but still returns true.
I was expecting supports
would return true
when all of the supplied dictionary members or enum values were known (but wouldn't, for example, return false
just because a required member wasn't supplied - supports
doesn't actually take the dictionary type itself). Then if a developer wants to determine the full set of supported options, she needs to call supports
once for each option she's interested in. I don't think there'd be any combinatorial scenario, i.e.:
(foo.supports({a:true}) && foo.supports({b:true}) ===
foo.supports({a:true, b:true})
I don't understand why we're squeamish about exposing dictionary names to the web. The names are already required to be unique in the WebIDL ecosystem, as they live in the same namespace as interfaces. The names are also usually quite long and weird, so collision chance is low.
I'm strongly against something that requires spec authors to affirmatively annotate their dictionary-taking APIs to make them dict-member-detectable, because that just means that most dict-taking APIs won't have it, and authors will have to remember yet another detail of the API (whether or not it exposes that feature-detection) before they try to use it, and there'll be browser differences in whether it's supported or not in a particular browser/version... All of this is bad and unnecessary when we can just somehow globally expose all dictionaries.
I don't care if we do it with a single global function like IDLDictSupports("dictName", "memberName")
instead of adding a bunch of dictionary names to the global, I just care that it works for everything automatically.
And anything that attaches the "is supported?" functionality to a given method assumes that methods will never take more than one dictionary argument, which is a broken assumption.
Yeah, if this is going to be specifically about feature-detecting whether an impl. supports a dictionary key, then I don't see why we would want to tie it to a particular method.
In addition to the multiple-dicts-per-method problem, you could also imagine two methods taking the same dictionary:
dictionary MyDict {
int mykey;
}
partial interface Navigator {
void method1(MyDict options);
void method2(MyDict options);
}
Then the developer has to decide which method to enquire about (even though they both return the same answer), and possibly get confused about whether they need to perform the enquiry twice:
if (method1.supports('mykey')) {
method1({mykey: ...});
}
if (method2.supports('mykey')) {
// Is the above check redundant? Can I just combine this into a single if statement?
method2({mykey: ...});
}
Probably best to just allow feature detection on the dict itself, not the method.
even though they both return the same answer
They might not, actually. The key might be supported in one method but not another. To be more precise, right now if a spec has:
dictionary SomeDict {
long key1;
long key2;
};
and that dictionary is used by two separate methods from two separate specs, and "key2" is a recent addition that the UA supports in one of the specs but not the other, then right now a UA can easily and transparently rewrite this in terms of two different dictionary types with different names, one with just key1, one with both keys. And UAs do that. This is a problem for the "let's just expose the dictionary name" thing too, for what it's worth.
Now of course the UA could just use SomeDict in the API where it doesn't actually support "key2" yet, but then support detection via getters would be broken too.
.supports()
relies on users calling it, outsourcing the real API problem rather than solving it IMHO.
That's my mea culpa 2 years after having implemented getSupportedConstraints:
What we wanted was this to fail on older browsers that didn't know about e.g. torch
:
track.applyConstraints({torch: {exact: true}}); // OverconstrainedError: what's torch?
What we got was:
if (!navigator.mediaDevices.getSupportedConstraints().torch) {
throw new DOMException("what's torch?", "OverconstrainedError");
}
track.applyConstraints({torch: {exact: true}});
It's a terrible API: People forget it, ignore it, or don't know about it, because their browser works.
It's also inherently unnecessary, except to work around WebIDL.
In hindsight, we could have specified object
instead of a MediaTrackConstraintSet
dictionary, with prose to copy it into a new MediaTrackConstraintSet
, and throw TypeError
on leftovers.ยน
I don't see why binding code couldn't do that for us. E.g.:
[ThrowOnUnknownMembers]
dictionary RequiredMediaTrackConstraints : MediaTrackConstraints {};
1) I'm simplifying. Constraints also defines ideal
which unlike exact
should be ignored, so getSupportedConstraints
is a lost cause.
@jan-ivar It's situational. What we're looking for in Web Share is an ability to detect whether it's going to work before calling the API. It's too late to throw an exception later.
The examples above don't do it justice (since they just check for errors immediately before). But for sharing images with Web Share you will be able to share an image thusly:
navigator.share({image: myImage});
If you are writing a drawing app and want a share button, you would ideally hide or grey out the button by detecting image sharing support before the user clicks it:
shareButton.disabled = !navigator.share.supports('image');
Otherwise, the user agent has no way to communicate to the website that image sharing will work until after the user clicks the button, which doesn't allow them to create a good user experience.
Yeah, both (letting some dictionaries, such as options dictionaries, throw on unknown, and letting people test for whether a key is known to a dictionary ahead of time) seem like reasonable use-cases to me. The former is best in general, as you can't screw it up by forgetting to check (or if you do, it'll fail in a much more obvious way on the user's side, and hopefully you're tracking unhandled exceptions), while the latter is useful when you do need to be proactive and know ahead of time what's available.
Sorry to bump this, but I haven't been able to find a good solution. My case is related with .focus()
and FocusOptions
, since I have a web where I want to focus to a specific input, while maintaining the ability to let the page scrolled (for example when an anchor link is referenced).
While I can patch the behaviour on my specific case, I was looking for the best way to detect if an engine supports FocusOptions
, to be able to write a polyfill for preventScroll
:
whatwg/html#2787
https://github.com/web-platform-tests/wpt/pull/7915/files#diff-65c854dfe02438ecb0bb6d0716c8d62b
Is there any way to do it?
Cheers!
The OP contains the current way to do this.
Where are we at with this? This came up in w3c/web-share#78 (actually the original context, adding a canShare
method for detection of new features). We could go ahead and add canShare
, but this discussion promises a more general method. It would be a shame if we standardized canShare
and then this happened.
So is this something that's likely to happen? Could we use Web Share as a test subject?
Why can't you use the method described in OP?
My take on that is that:
- That method is very non-obvious and people can easily mess it up.
- That method relies on being able to call the relevant API in a side-effect-free way (whether due to the API steps or due to ensuring the API throws), which may not always be possible.
Why would 2 not be possible? The IDL layer can always throw, right? So throw something IDL would never throw, like 1
. It seems hard for that to have a side-effect since at that point the specification algorithm hasn't run.
And although 1 is non-obvious, it seems better than maintaining a "supported features" side-table, which we've known historically to not work. (Perhaps we can do better now with better tests, but hmm...)
Why would 2 not be possible? The IDL layer can always throw, right? So throw something IDL would never throw, like 1
OK, but what happens in a UA that doesn't support that dictionary member?
and ignore subtle bugs with old IE
And this is bad because...?
OK, but what happens in a UA that doesn't support that dictionary member?
@bzbarsky Good point. Though in the case of share() at least, an invalid url
should do it:
var supportsFiles = false;
try {
await navigator.share(Object.defineProperty({url: ' '}, 'files', {get: function () {
supportsFiles = true;
}}));
} catch(e) {}
Right, for any specific API you can probably find a way. But it requires some creative thought every time, and is too easy to mess up.
Note that I am not a huge fan of the out-of-band list either. The only good news is that I think all browsers could code-generate that list from the same IDL that they use to code-generate their dictionary implementations. So apart from bugs in the code generators, some of the issues with the list not matching reality that we had in the past would probably be avoidable.
Yeah, OP's code is pretty insanely complex, and the fact that you actually have to custom-design it for every single feature (no generic pattern will work) makes it even worse, because it means you can't just abstract the test away into some library. We really do need to solve this generically via WebIDL.
I feel there's a discrepancy between the 5 methods identified that need this, and the 400+ dictionaries in the platform.
Maybe it's not too much to ask of individual methods to think about this?
Those five cases aren't distinguished in any particular way - the need exists any time we add to a dictionary. Most dictionaries won't be expanded, true; they generally get defined once and then never touched again. But any dictionary could be expanded. Having to recognize which ones have been expanded, and figure out the unique key-testing method that each one has hopefully remembered to define for itself as soon as the first editor added to the dictionary, is not a good thing to foist on authors, and not a particularly dependable thing to require spec authors to remember to do.
If we're afraid of putting all the dictionary names on the global, we can always stuff it into a single method that takes a dictionary name and a key name, so there's no chance of future collisions.
It honestly took me about 10 minutes to even understanding how that method works. To clarify, I assume it works by:
- Calling
navigator.share
with{url: ' '}
, i.e., an invalid URL, which should according to theshare
spec reject with aTypeError
. - Injecting logic to determine whether a "
files
" attribute was accessed on the dictionary passed toshare
, before it threw aTypeError
.
I can see quite a lot of problems with this approach:
- It assumes that the user agent's URL parser works correctly and rejects
" "
. While that should be the case, it means your Web Share feature detection is dependent on an edge case of the URL parser working correctly, which is notoriously non-compliant in all known user agents. [Note: In Chrome 69, this actually succeeds. I don't know why; we do fail more complex invalid URLs, but this one passes.] - It assumes that the user agent invokes getters on a dictionary when a built-in function accesses its attributes. Again, this should be the case (?), but you're increasing the surface area for bugs in the user agent to mess with your feature detection.
- It assumes that the getter is invoked when the implementation merely checks for the presence of an attribute. (I don't know enough about
defineProperty
to know whether this is supposed to happen or not.) This feels implementation-dependent, depending on whether it gets the value offiles
, or merely uses something equivalent tohasOwnProperty
to check for the existence of a "files
" attribute. - It assumes that the implementation checks the presence of the "
files
" attribute before checking whether theurl
is valid. The spec text says to reject if no known property is present, before checking the URL, but it seems brittle for a web developer to rely on the implementation doing those checks in the right order. - It assumes that an implementation that reads the "
files
" attribute supports file sharing. That isn't necessarily true: I can imagine, for example, that we could gather metrics on how many developers are using "files
" in their share dictionaries (thus triggering the getter) without actually supporting file sharing. - It's extremely arcane. It's essentially impossible for a web developer to invent it by themself, and difficult to understand once you see it, so they just have to find it in documentation somewhere and copy+paste it, trusting that it works.
[Note: Other than the above issue where url: ' '
is accepted, if we change to a more complex invalid case: url: 'https://example.com:65536/'
, the above approach does work with navigator.share
on Chrome 69 for Android.]
The above issues are specific to share
, but they illustrate that this approach may have similar issues when applied to other APIs, or not even be possible (imagine if we hadn't invoked the URL parser, then the trick wouldn't even work).
I would rather provide an explicit API to detect whether things are supported, than the above approach which amounts to developers relying on observing "implementation details" (whether those are user-agent-specific details or just the quirks of the spec text) to find out whether it's supported. In my mind, the question is: should we go ahead and make a Web-Share-specific feature detection API, or should we scope out a general one that applies to any dictionary type?
It assumes that the user agent invokes getters on a dictionary when a built-in function accesses its attributes.
This is a very clear requirement in the Web IDL spec, there are tests for it using several dictionary types, and I am pretty sure all browsers get this right. There really shouldn't be failures here.
It assumes that the getter is invoked when the implementation merely checks for the presence of an attribute.
No, it assumes that the getter is invoked as part of Web IDL argument processing. Which, again, is a basic Web IDL spec requirement.
It assumes that the implementation checks the presence of the "files" attribute before checking whether the url is valid.
No. The presence check and the URL check both happen in the actual algorithm for the method, which runs after all the Web IDL argument processing is complete. There is no assumption being made here other than correct implementation of Web IDL method calls in general.
In general, all of points 2, 3, 4 come down to "a UA could have totally broken Web IDL argument handling", which I suspect is strictly less likely than "a UA could have its out-of-band support claims not match its actual support".
I can imagine, for example, that we could gather metrics on how many developers are using "files" in their share dictionaries (thus triggering the getter) without actually supporting file sharing.
This one is a bit odd, actually. Adding support for properties in dictionaries in observable ways (including calling getters) without actually supporting those properties is something that generally gets frowned on.
For purposes of the sort of telemetry you describe, I would personally implement it in terms of internal engine APIs that promised to not have side effects (e.g. not calling getters or proxy handlers). It would undercount uses that relied on such constructs, but I would not expect web developers to typically use those in dictionaries anyway.
I do agree with your points 1 and 6, which basically come down to my two concerns above: it's hard to make sure you call the API in a side-effect-free way and the whole thing is highly non-obvious. The big question for some people is whether those are enough to outweigh the extra likelihood of broken out-of-band support claims.
The above issues are specific to
share
Only issue 1 is really specific to share
in its specific phrasing.
OK thanks @bzbarsky for explaining those. I now understand that the call to the getter happens during the IDL processing (converting a JavaScript Object to a ShareData
value), before any of the Web Share algorithm steps take place, and thus the order of the steps is irrelevant. Therefore, my points 2โ5 are invalid, and this is actually a much better reflection of whether the files
attribute exists in the implementation's IDL dictionary than I had previously thought.
I still stand by points 1 and 6. This is still relying on URL processing to reject before share
has any side effects.
The reason I hesitate, is I don't consider a dictionary to be a first class API.
Someone once told me, "you don't have to use dictionaries. Use object
. Problem solved." Their point being (I think) was that WebIDL defines dictionaries to encourage certain API patterns and discourage others. Therefore, it's not always sufficient to have a problem. The question is: is it a good pattern? For example, is share() really several methods in one? Might a discrete shareFile() method have been cleaner? I'm not saying it is, just using it as an example.
For example, is share() really several methods in one? Might a discrete shareFile() method have been cleaner? I'm not saying it is, just using it as an example.
That decision isn't fixed in stone yet. It's part of this proposal:
http://wicg.github.io/web-share/level-2/
Maybe, but we also want to be able to share a file along with the other metadata at the same time (e.g., share a URL along with a screenshot).
What I'm asking here, at a high level, is: should there be a standardized pattern for how to do this kind of sub-feature detection, or should each API build its own? The suggestion to use a separate shareFile
method suggests that each API should be finding its own way of supplying the new features.
Not sure if this was proposed yet, but how about stuffing the supported options onto the functions/methods that take them?
If I want to know how to call someElem.addEventListener(...)
, I'll probe someElem.addEventListener.supportedOptions
or someElem.addEventListener.AddEventListenerOptions
or something like that.
Minimal namespace pollution, and a pretty obvious place to look?
(For dictionaries taken by multiple functions, or methods that exist on multiple objects, such as .addEventListener
, the specific standard should probably promise that the supported options are same for all places. Or not.)
I'm working with a customer who wants this kind of detection (with an origin trial, FYI).
I think this problem is not only for dictionary but function signature detection like "if this function supports boolean as the 3rd parameter?".
This reminds me System.Reflection on .Net C#.
I know they manages this kind of detection because of C# is strongly typed, but can WebIDL support this?
Has there been any progress on this? We recently discussed w3c/performance-timeline#176 in a call and someone pointed out this issue, which would fix the problem for us.
In an unrelated thread, @mkruisselbrink points out that it's possible to feature detect dictionary members by providing the method an object with a getter and seeing if it was read.
That's cumbersome, but could work...
Yep, that's the pattern I used when opening this issue. We continue to hear developers complain that they really don't consider that acceptable (complexity, obscurity, desire to reliably avoid exceptions, etc).
Apologies for not actually reading the OP...
Agree that a simpler method would be better.
I had an "exotic idea" come into my mind about this issue, I hope that after 5 years it's ok to propose such ideas, at least to make things move a bit.
Note that I am really not proficient in WebIDL and thus not entirely sure of what can and can't be done, so this proposal may very well be completely unfeasible, if so, sorry in advance.
Also, this assumes ES binding, which might once again be a completely wrong assumption.
Anyway, the idea would be to expose a new global method (name can totally change, I'm really bad at naming)
interface mixin WindowOrWorkerGlobalScope { // or any global context really
+ boolean methodSupports(Function method, any... arguments);
}
Where web authors would pass directly the ECMAScript function object they have access to as the first argument and the arguments they'd like to test as the following arguments.
Each argument would be tested against the UA's IDL implementation to see if it is recognized as potentially valid.
For instance to test for Worker's type = "module"
support, one would write
globalThis.methodSupports(Worker, "" /* the URL param */, { type: "module" } /* the option param */)
To avoid creating void objects only to pass the required arguments, maybe undefined
or null
values should be ignored from the tests, e.g
globalThis.methodSupports(EventTarget.prototype.addEventListener, null, null, { signal } );
would ignore both the event type and callback params but check if signal
is an AbortSignal.
If the arguments length is bigger than the one expected, or if a dictionary member is not recognized, or if the value of an enum is not valid, the method should return false
, otherwise it should return true
(so it doesn't look if required options are passed or not, it only checks that the passed ones are recognized).
I believe such a model would solve the issues outlined with the two proposed solutions so far:
- No need to expose every dictionary, nor to fix their names nor even to reify them in any other way than what is currently done.
- Can be automatically added to any method, no need to subscribe at definition (though some definitions may want their own validation steps?)
- Obviously works for many methods defined on the same interface
- Works for both enums and dictionary options
However it also comes with its own questions (and many others I probably didn't see myself)
- Can WebIDL link ECMAScript function objects to the actual IDL method? Even if there isn't any mechanism today, could one be built?
- What about all the methods that are language specific and not linked to IDL? e.g should this method also work for ES
Intl.numberFormats
andArray.from
and everything else? I doubt many web authors are interested in where a method has been specced from, they will probably not understand why some methods are excluded. - How should DOMStrings be tested? Many methods will actually fail on invalid values, for instance the
HTMLCanvasElement.getContext()
method uses a DOMString instead of an enum and returnsnull
when the value provided isn't recognized, shouldmethodSupports(canvas.getContext, "webgpu")
returntrue
even in UAs that don't support this context type? - Is the difference constructor vs method calls gonna be a problem?
Random idea on my bed: constructors for dictionaries
dictionary Foo {
(DOMString or boolean) bar;
} ;
// implicitly creates an interface-like constructor
"bar" in Foo.prototype // typical existence check
new Foo({ bar: "bar" }).bar === "bar" // type support check; string in this case
Problem: massive namespace pollution ๐ค
Workaround 1: Add createDictionary(name, dict)
like document.createEvent()
and hide the constructors as [LegacyNoInterfaceObject]
does.
This idea of a createDictionary(name, dict)
method sounds like a variation of the solution proposed in the OP and faces the same issues expressed in #107 (comment).
The most convincing argument against this model for me is that specs authors should be able to change these dictionaries in ways that would break this model.
As an example, the very EventListenerOptions
dictionary this issue originally came from has been changed just seven days after this issue got open to be split in two dictionaries, and a few days later it went from holding two properties to just one. An author today would have to test for AddEventListenerOptions
, but a website written in the intermediary time-span would get false negatives by still looking for EventListenerOptions
.
@Kaiido I quite like that idea, but it does seem like a lot of work for something that can be solved with throwing getters. And as you note it doesn't work for all the things you might want to find out support for. Based on that I'd still lean toward throwing getters and purpose-built support()
methods where needed.
@annevk But as has been said previously, "throwing getters" actually do not work or at least they only work in environments that do support an option that is alphabetically after the one you want to test. If you want to test them all, or the last one alphabetically, it will execute the method in environments that don't support this option.
As to .support()
, it works only if there is a single method per interface...
...(๐ก) Unless we plan to add it on the methods directly? But this still sounds very exotic and I fear we'll still face the "why isn't there an Intl.numberFormat.support()
" problem, though maybe less prominently, and I guess it would be easier to implement than the global methodSupports()
๐ค
As linked above, foolip/mdn-bcd-collector#1485 is about adding detection for parameters to mdn-bcd-collector, as I'm aware at least some data in BCD that is outdated because it's non-trivial to do this in a general way. Incorrect data in BCD does, inevitably, further hurt web developers (above and beyond their specific problems with feature detection).
Clearly as a general purpose solution we can't rely on the alphabetically last dictionary member being supported (nor does that allow the last one to be tested).
#107 (comment) makes sense to me, and still I'd prefer a general solution for automation purpose e.g. BCD mentioned above.
My second random thought, which also covers signature and argument support detection:
// Given this IDL:
interface Bar {
foo(Bar bar);
foo((boolean or DOMString) union);
foo(float f, unsigned short s);
foo(Baz baz);
};
dictionary Bar {
DOMString bar;
};
enum Baz { "baz", "" };
Foo.prototype.foo[Symbol.inspect];
inspectSignature(Foo.prototype.foo);
// returns:
// [
// { bar: String },
// [[Boolean, String]],
// [Number, Number]
// [["baz", ""]]
// ]
(Not sure how to differentiate optional arguments though.)