Skip to content

Commit

Permalink
Return AbortSignal instead of {destroy()} + support signal (#28)
Browse files Browse the repository at this point in the history
Co-authored-by: Federico Brigante <[email protected]>
  • Loading branch information
cheap-glitch and fregante authored Jun 23, 2022
1 parent a0a60d8 commit 4f3d7ea
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 64 deletions.
71 changes: 32 additions & 39 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ export type EventType = keyof GlobalEventHandlersEventMap;
type GlobalEvent = Event;

namespace delegate {
export type Subscription = {
destroy: VoidFunction;
};

export type EventHandler<
TEvent extends GlobalEvent = GlobalEvent,
TElement extends Element = Element,
Expand Down Expand Up @@ -95,7 +91,7 @@ function delegate<
type: TEventType,
callback: delegate.EventHandler<GlobalEventHandlersEventMap[TEventType], TElement>,
options?: DelegateOptions
): delegate.Subscription;
): AbortController;

function delegate<
TElement extends Element = HTMLElement,
Expand All @@ -106,7 +102,7 @@ function delegate<
type: TEventType,
callback: delegate.EventHandler<GlobalEventHandlersEventMap[TEventType], TElement>,
options?: DelegateOptions
): delegate.Subscription;
): AbortController;

// This type isn't exported as a declaration, so it needs to be duplicated above
function delegate<
Expand All @@ -118,32 +114,37 @@ function delegate<
type: TEventType,
callback: delegate.EventHandler<GlobalEventHandlersEventMap[TEventType], TElement>,
options?: DelegateOptions,
): delegate.Subscription {
): AbortController {
const internalController = new AbortController();
const listenerOptions: AddEventListenerOptions = typeof options === 'object' ? options : {capture: options};
// Drop unsupported `once` option https://github.com/fregante/delegate-it/pull/28#discussion_r863467939
delete listenerOptions.once;

if (listenerOptions.signal) {
if (listenerOptions.signal.aborted) {
internalController.abort();
return internalController;
}

listenerOptions.signal.addEventListener('abort', () => {
internalController.abort();
});
} else {
listenerOptions.signal = internalController.signal;
}

// Handle Selector-based usage
if (typeof base === 'string') {
base = document.querySelectorAll(base);
}

// Handle Array-like based usage
if (!isEventTarget(base)) {
const subscriptions = Array.prototype.map.call(
base,
(element: EventTarget) => delegate(
element,
selector,
type,
callback,
options,
),
) as delegate.Subscription[];

return {
destroy(): void {
for (const subscription of subscriptions) {
subscription.destroy();
}
},
};
Array.prototype.forEach.call(base, element => {
delegate(element, selector, type, callback, listenerOptions);
});

return internalController;
}

// `document` should never be the base, it's just an easy way to define "global event listeners"
Expand All @@ -159,25 +160,17 @@ function delegate<
}
};

// Drop unsupported `once` option https://github.com/fregante/delegate-it/pull/28#discussion_r863467939
if (typeof options === 'object') {
delete (options as AddEventListenerOptions).once;
}

const setup = JSON.stringify({selector, type, capture});
const isAlreadyListening = editLedger(true, baseElement, callback, setup);
const delegateSubscription = {
destroy() {
baseElement.removeEventListener(type, listenerFn, options);
editLedger(false, baseElement, callback, setup);
},
};

if (!isAlreadyListening) {
baseElement.addEventListener(type, listenerFn, options);
baseElement.addEventListener(type, listenerFn, listenerOptions);
}

return delegateSubscription;
internalController.signal.addEventListener('abort', () => {
editLedger(false, baseElement, callback, setup);
});

return internalController;
}

export default delegate;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"devDependencies": {
"@sindresorhus/tsconfig": "^2.0.0",
"ava": "^4.2.0",
"jsdom": "^19.0.0",
"jsdom": "^20.0.0",
"npm-run-all": "^4.1.5",
"sinon": "^14.0.0",
"typescript": "^4.6.4",
Expand Down
16 changes: 14 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This is a fork of the popular [`delegate`](https://github.com/zenorocha/delegate
- modern: ES6, TypeScript, Edge 15+ (it uses `WeakMap` and `Element.closest()`)
- idempotent: identical listeners aren't added multiple times, just like the native `addEventListener`
- debugged ([2d54c11](https://github.com/fregante/delegate-it/commit/2d54c1182aefd3ec9d8250fda76290971f5d7166), [c6bb88c](https://github.com/fregante/delegate-it/commit/c6bb88c2aa8097b25f22993a237cf09c96bcbfb8))
- supports [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)

If you need IE support, you can keep using [`delegate`](https://github.com/zenorocha/delegate)

Expand Down Expand Up @@ -72,11 +73,22 @@ delegate(document.body, '.btn', 'click', event => {
### Remove event delegation

```js
const delegation = delegate(document.body, '.btn', 'click', event => {
const controller = delegate(document.body, '.btn', 'click', event => {
console.log(event.delegateTarget);
});

delegation.destroy();
controller.abort();
```

```js
const controller = new AbortController();
delegate(document.body, '.btn', 'click', event => {
console.log(event.delegateTarget);
}, {
signal: controller.signal,
});

controller.abort();
```

### Custom event types in Typescript
Expand Down
88 changes: 66 additions & 22 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ global.Event = window.Event;
global.Element = window.Element;
global.Document = window.Document;
global.MouseEvent = window.MouseEvent;
global.AbortController = window.AbortController;
global.document = window.document;
const container = window.document.querySelector('ul');
const anchor = window.document.querySelector('a');
Expand Down Expand Up @@ -51,13 +52,24 @@ test.serial('should handle events on text nodes', t => {
});

test.serial('should remove an event listener', t => {
const spy = sinon.spy(container, 'removeEventListener');
const spy = sinon.spy();
const controller = delegate(container, 'a', 'click', spy);
controller.abort();

const anchor = document.querySelector('a');
anchor.click();
t.true(spy.notCalled);
});

const delegation = delegate(container, 'a', 'click', () => {});
delegation.destroy();
test.serial('should pass an AbortSignal to an event listener', t => {
const spy = sinon.spy();
const controller = new AbortController();
delegate(container, 'a', 'click', spy, {signal: controller.signal});
controller.abort();

t.true(spy.calledOnce);
spy.restore();
const anchor = document.querySelector('a');
anchor.click();
t.true(spy.notCalled);
});

test.serial('should add event listeners to all the elements in a base selector', t => {
Expand All @@ -71,17 +83,28 @@ test.serial('should add event listeners to all the elements in a base selector',
});

test.serial('should remove the event listeners from all the elements in a base selector', t => {
const items = document.querySelectorAll('li');
const spies = Array.prototype.map.call(items, li => sinon.spy(li, 'removeEventListener'));
const spy = sinon.spy();
const controller = delegate('li', 'a', 'click', spy);
controller.abort();

const delegation = delegate('li', 'a', 'click', () => {});
delegation.destroy();
for (const anchor of document.querySelectorAll('a')) {
anchor.click();
}

t.true(spies.every(spy => {
const success = spy.calledOnce;
spy.restore();
return success;
}));
t.true(spy.notCalled);
});

test.serial('should pass an AbortSignal to the event listeners on all the elements in a base selector', t => {
const spy = sinon.spy();
const controller = new AbortController();
delegate('li', 'a', 'click', spy, {signal: controller.signal});
controller.abort();

for (const anchor of document.querySelectorAll('a')) {
anchor.click();
}

t.true(spy.notCalled);
});

test.serial('should add event listeners to all the elements in a base array', t => {
Expand All @@ -96,17 +119,30 @@ test.serial('should add event listeners to all the elements in a base array', t
});

test.serial('should remove the event listeners from all the elements in a base array', t => {
const spy = sinon.spy();
const items = document.querySelectorAll('li');
const spies = Array.prototype.map.call(items, li => sinon.spy(li, 'removeEventListener'));
const controller = delegate(items, 'a', 'click', spy);
controller.abort();

const delegation = delegate(items, 'a', 'click', () => {});
delegation.destroy();
for (const anchor of document.querySelectorAll('a')) {
anchor.click();
}

t.true(spies.every(spy => {
const success = spy.calledOnce;
spy.restore();
return success;
}));
t.true(spy.notCalled);
});

test.serial('should pass an AbortSignal to the event listeners on all the elements in a base array', t => {
const spy = sinon.spy();
const items = document.querySelectorAll('li');
const controller = new AbortController();
delegate(items, 'a', 'click', spy, {signal: controller.signal});
controller.abort();

for (const anchor of document.querySelectorAll('a')) {
anchor.click();
}

t.true(spy.notCalled);
});

test.serial('should not fire when the selector matches an ancestor of the base element', t => {
Expand All @@ -117,6 +153,14 @@ test.serial('should not fire when the selector matches an ancestor of the base e
t.true(spy.notCalled);
});

test.serial('should not add an event listener when passed an already aborted signal', t => {
const spy = sinon.spy(container, 'addEventListener');
delegate(container, 'a', 'click', spy, {signal: AbortSignal.abort()});

anchor.click();
t.true(spy.notCalled);
});

test.serial('should not consider the `once` option', t => {
const spy = sinon.spy();
delegate(container, 'a', 'click', spy, {once: true});
Expand Down

0 comments on commit 4f3d7ea

Please sign in to comment.