Skip to content

Instantly share code, notes, and snippets.

@samthor
Last active June 22, 2024 18:10
Show Gist options
  • Save samthor/2e11de5976fe673557b0ee14a3cb621a to your computer and use it in GitHub Desktop.
Save samthor/2e11de5976fe673557b0ee14a3cb621a to your computer and use it in GitHub Desktop.
Polyfill/code for the signal argument to addEventListener
// This is part of the blog post here: https://whistlr.info/2022/abortcontroller-is-your-friend/
// ...and can be used to detect/polyfill the `signal` argument to addEventListener.
//
// Note that at writing, 86%+ of active browsers already support it:
// https://caniuse.com/mdn-api_eventtarget_addeventlistener_options_parameter_options_signal_parameter
// ...but that 92% of browsers support `AbortController` and signal.
//
// So there's 6% of total browsers which will fail silently when adding the `signal` argument.
// Eyeballing it, this is mostly Safari 11-15 and Chrome 66-90. These snippets can help with those targets.
//
// If there's interest in making this a proper library, I may do that. Hit me up on
// Twitter (https://twitter.com/samthor) or leave a comment.
/**
* Checks whether the current environment supports the `signal` argument to addEventListener.
*/
export function hasEventListenerSignalSupport() {
if (typeof AbortController === 'undefined') {
return false; // doesn't even support AbortController :(
}
const c = new AbortController();
c.abort();
let signalOnListenerSupport = true;
globalThis.addEventListener('_test', () => {
// If this fires even though the controller is aborted, there's no support
signalOnListenerSupport = false;
}, { signal: c.signal });
globalThis.dispatchEvent(new CustomEvent('_test'));
return signalOnListenerSupport;
}
/**
* Adds support for the `signal` argument to addEventListener. Throws error if
* {@link AbortController} is not supported at all.
*/
export function maybeAddEventListenerSignalPolyfill() {
if (typeof AbortController === 'undefined') {
throw new Error(`can't add, AbortController not supported`);
}
if (hasEventListenerSignalSupport()) {
return;
}
const orig = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(eventName, fn, options) {
const self = this;
if (options?.signal) {
if (!(options.signal instanceof AbortSignal)) {
throw new Error(`unexpected type (not AbortSignal) for signal arg`);
}
if (options.signal.aborted) {
return; // do nothing, already aborted
}
// copy so user can't change us: unlike the fn, the options arg can change, as
// long as it has the same values
const localOptions = {...options};
options.signal.addEventListener('abort', () => {
self.removeEventListener(eventName, fn, localOptions);
});
}
return orig.call(this, eventName, fn, options);
};
}
@nuxodin
Copy link

nuxodin commented Jun 28, 2022

Works great!

I rewrote it a bit and added it to my polyfill-service:
https://github.com/nuxodin/lazyfill/blob/main/monkeyPatches/addEventListenerSignal.js

a few possible improvements:

  • easier support check
  • calling typeof AbortControler will fail if not define (i think)
  • self = this There is no need for this as there are arrow-functions
!function(){
    let supported = false;
    document.createElement('i').addEventListener('click',()=>{}, {
        get signal() { supported = true; },
    });
    if (supported) return;
    if (!window.AbortController) throw new Error(`AbortController not supported`);
    const orig = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function (eventName, fn, options) {
        if (options && options.signal) {
            if (options.signal.aborted) return; // do nothing, already aborted
            options.signal.addEventListener('abort', () => this.removeEventListener(eventName, fn, { ...options }) );
        }
        return orig.call(this, eventName, fn, options);
    };
}();

@samthor
Copy link
Author

samthor commented Jun 28, 2022

@nuxodin great! I'd still copy options though:

const options = { signal };
foo.addEventListener('whatever', fn, options);
options.capture = true;

// later
signal.abort();  // won't work, 'capture' is part of event

You could also:

const localOptions = { capture: !!options.capture };

since that is the important part.

@nuxodin
Copy link

nuxodin commented Jun 29, 2022

Ok, added it, thanks for your feedback.

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