Skip to content

Commit

Permalink
Add support for once (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
fregante authored Apr 22, 2023
1 parent a968bc3 commit 39dcce7
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 26 deletions.
15 changes: 10 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {ParseSelector} from 'typed-query-selector/parser.d.js';

export type DelegateOptions = boolean | Omit<AddEventListenerOptions, 'once'>;
export type DelegateOptions = boolean | AddEventListenerOptions;
export type EventType = keyof GlobalEventHandlersEventMap;

export type DelegateEventHandler<
Expand Down Expand Up @@ -72,7 +72,7 @@ function safeClosest(event: Event, selector: string): Element | void {

/**
* Delegates event to a selector.
* @param options A boolean value setting options.capture or an options object of type AddEventListenerOptions without the `once` option
* @param options A boolean value setting options.capture or an options object of type AddEventListenerOptions
*/
function delegate<
Selector extends string,
Expand Down Expand Up @@ -109,8 +109,6 @@ function delegate<
options?: DelegateOptions,
): void {
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;

const {signal} = listenerOptions;

Expand All @@ -132,6 +130,9 @@ function delegate<
return;
}

// Don't pass `once` to `addEventListener` because it needs to be handled in `delegate-it`
const {once, ...nativeListenerOptions} = listenerOptions;

// `document` should never be the base, it's just an easy way to define "global event listeners"
const baseElement = base instanceof Document ? base.documentElement : base;

Expand All @@ -142,13 +143,17 @@ function delegate<
if (delegateTarget) {
const delegateEvent = Object.assign(event, {delegateTarget});
callback.call(baseElement, delegateEvent as DelegateEvent<GlobalEventHandlersEventMap[TEventType], TElement>);
if (once) {
baseElement.removeEventListener(type, listenerFn, nativeListenerOptions);
editLedger(false, baseElement, callback, setup);
}
}
};

const setup = JSON.stringify({selector, type, capture});
const isAlreadyListening = editLedger(true, baseElement, callback, setup);
if (!isAlreadyListening) {
baseElement.addEventListener(type, listenerFn, listenerOptions);
baseElement.addEventListener(type, listenerFn, nativeListenerOptions);
}

signal?.addEventListener('abort', () => {
Expand Down
10 changes: 9 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,15 @@ delegate(document.body, '.btn', 'click', event => {
});
```

**Note:** the `once` option is currently not supported.
#### Listen to one event only

```js
delegate(document.body, '.btn', 'click', event => {
console.log('This will only be called once');
}, {
once: true
});
```

### Remove event delegation

Expand Down
60 changes: 40 additions & 20 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,6 @@ test.serial('should add an event listener', t => {
anchor.click();
});

test.serial('should add an event listener only once', t => {
t.plan(2);

// Only deduplicates the `capture` flag
// https://github.com/fregante/delegate-it/pull/11#discussion_r285481625

// Capture: false
delegate(container, 'a', 'click', t.pass);
delegate(container, 'a', 'click', t.pass, {passive: true});
delegate(container, 'a', 'click', t.pass, {capture: false});

// Capture: true
delegate(container, 'a', 'click', t.pass, true);
delegate(container, 'a', 'click', t.pass, {capture: true});

anchor.click();
});

test.serial('should handle events on text nodes', t => {
delegate(container, 'a', 'click', t.pass);
anchor.firstChild.dispatchEvent(new MouseEvent('click', {bubbles: true}));
Expand Down Expand Up @@ -164,11 +146,49 @@ test.serial('should not add an event listener when passed an already aborted sig
t.true(spy.notCalled);
});

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

container.click();
t.true(spy.notCalled, 'It should not be called on the container');
anchor.click();
t.true(spy.calledOnce, 'It should be called on the delegate target');
anchor.click();
t.true(spy.calledTwice);
t.true(spy.calledOnce, 'It should not be called again on the delegate target');
});

test.serial('should add a specific event listener only once', t => {
t.plan(2);

// Only deduplicates the `capture` flag
// https://github.com/fregante/delegate-it/pull/11#discussion_r285481625

// Capture: false
delegate(container, 'a', 'click', t.pass);
delegate(container, 'a', 'click', t.pass, {passive: true});
delegate(container, 'a', 'click', t.pass, {capture: false});

// Capture: true
delegate(container, 'a', 'click', t.pass, true);
delegate(container, 'a', 'click', t.pass, {capture: true});

// Once
delegate(container, 'a', 'click', t.pass, {once: true});
delegate(container, 'a', 'click', t.pass, {once: false});

anchor.click();
});

test.serial('should deduplicate identical listeners added after `once:true`', t => {
const spy = sinon.spy();
delegate(container, 'a', 'click', spy, {once: true});
delegate(container, 'a', 'click', spy, {once: false});

container.click();
t.true(spy.notCalled, 'It should not be called on the container');
anchor.click();
t.true(spy.calledOnce, 'It should be called on the delegate target');
anchor.click();
t.true(spy.calledOnce, 'It should not be called again on the delegate target');
});

0 comments on commit 39dcce7

Please sign in to comment.