Skip to content

Commit

Permalink
Always return a single subscription, never an array
Browse files Browse the repository at this point in the history
  • Loading branch information
fregante committed Apr 7, 2020
1 parent 7c9b6c3 commit 357bcc2
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 62 deletions.
92 changes: 38 additions & 54 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,43 @@ namespace delegate {
};
}

const elements = new WeakMap<EventTarget, WeakMap<delegate.DelegateEventHandler<any, any>, Set<delegate.Setup>>>();
const ledger = new WeakMap<EventTarget, WeakMap<delegate.DelegateEventHandler<any, any>, Set<delegate.Setup>>>();

function _delegate<TElement extends Element = Element, TEvent extends Event = Event>(
element: EventTarget,
function isEventTarget(elements: EventTarget | Document | ArrayLike<Element> | string): elements is EventTarget {
return typeof (elements as EventTarget).addEventListener === 'function';
}

/**
* Delegates event to a selector.
*/
function delegate<TElement extends Element = Element, TEvent extends Event = Event>(
base: EventTarget | Document | ArrayLike<Element> | string,
selector: string,
type: delegate.EventType,
callback: delegate.DelegateEventHandler<TEvent, TElement>,
options?: boolean | AddEventListenerOptions
): delegate.DelegateSubscription {
// 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) => {
return delegate<TElement, TEvent>(element, selector, type, callback, options);
}) as delegate.DelegateSubscription[];

return {
destroy(): void {
subscriptions.forEach(subscription => subscription.destroy());
}
};
}

const baseElement = base; // Required for TypeScript

// Handle the regular Element usage
const capture = Boolean(typeof options === 'object' ? options.capture : options);
const listenerFn: EventListener = (event: Partial<delegate.DelegateEvent>): void => {
const delegateTarget = (event.target as Element).closest(selector) as TElement;
Expand All @@ -40,18 +68,18 @@ function _delegate<TElement extends Element = Element, TEvent extends Event = Ev
// Closest may match elements outside of the currentTarget
// so it needs to be limited to elements inside it
if ((event.currentTarget as Element).contains(event.delegateTarget)) {
callback.call(element, event as delegate.DelegateEvent<TEvent, TElement>);
callback.call(baseElement, event as delegate.DelegateEvent<TEvent, TElement>);
}
};

const delegateSubscription = {
destroy() {
element.removeEventListener(type, listenerFn, options);
if (!elements.has(element)) {
baseElement.removeEventListener(type, listenerFn, options);
if (!ledger.has(baseElement)) {
return;
}

const elementMap = elements.get(element)!;
const elementMap = ledger.get(baseElement)!;
if (!elementMap.has(callback)) {
return;
}
Expand Down Expand Up @@ -81,7 +109,7 @@ function _delegate<TElement extends Element = Element, TEvent extends Event = Ev
}
};

const elementMap = elements.get(element) ?? new WeakMap<delegate.DelegateEventHandler<TEvent, TElement>, Set<delegate.Setup>>();
const elementMap = ledger.get(baseElement) ?? new WeakMap<delegate.DelegateEventHandler<TEvent, TElement>, Set<delegate.Setup>>();
const setups = elementMap.get(callback) ?? new Set<delegate.Setup>();
for (const setup of setups) {
if (
Expand All @@ -94,60 +122,16 @@ function _delegate<TElement extends Element = Element, TEvent extends Event = Ev
}

// Remember event in tree
elements.set(element,
ledger.set(baseElement,
elementMap.set(callback,
setups.add({selector, type, capture})
)
);

// Add event on delegate
element.addEventListener(type, listenerFn, options);
baseElement.addEventListener(type, listenerFn, options);

return delegateSubscription;
}

// Single base element specified
function delegate<TElement extends Element = Element, TEvent extends Event = Event>(
elements: EventTarget | Document,
selector: string,
type: delegate.EventType,
callback: delegate.DelegateEventHandler<TEvent, TElement>,
options?: boolean | AddEventListenerOptions
): delegate.DelegateSubscription;

// Array(-like) of elements or selector string
function delegate<TElement extends Element = Element, TEvent extends Event = Event>(
elements: ArrayLike<Element> | string,
selector: string,
type: delegate.EventType,
callback: delegate.DelegateEventHandler<TEvent, TElement>,
options?: boolean | AddEventListenerOptions
): delegate.DelegateSubscription[];

/**
* Delegates event to a selector.
*/
function delegate<TElement extends Element = Element, TEvent extends Event = Event>(
elements: EventTarget | Document | ArrayLike<Element> | string,
selector: string,
type: delegate.EventType,
callback: delegate.DelegateEventHandler<TEvent, TElement>,
options?: boolean | AddEventListenerOptions
): any {
// Handle the regular Element usage
if (typeof (elements as EventTarget).addEventListener === 'function') {
return _delegate<TElement, TEvent>(elements as EventTarget, selector, type, callback, options);
}

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

// Handle Array-like based usage
return Array.prototype.map.call(elements, (element: EventTarget) => {
return _delegate<TElement, TEvent>(element, selector, type, callback, options);
});
}

export = delegate;
12 changes: 4 additions & 8 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,8 @@ test('should remove the event listeners from all the elements in a base selector
return sinon.spy(li, 'removeEventListener');
});

const delegations = delegate('li', 'a', 'click', () => {});
delegations.forEach(delegation => {
delegation.destroy();
});
const delegation = delegate('li', 'a', 'click', () => {});
delegation.destroy();

t.true(spies.every(spy => {
const success = spy.calledOnce;
Expand All @@ -97,10 +95,8 @@ test('should remove the event listeners from all the elements in a base array',
return sinon.spy(li, 'removeEventListener');
});

const delegations = delegate(items, 'a', 'click', () => {});
delegations.forEach(delegation => {
delegation.destroy();
});
const delegation = delegate(items, 'a', 'click', () => {});
delegation.destroy();

t.true(spies.every(spy => {
const success = spy.calledOnce;
Expand Down

0 comments on commit 357bcc2

Please sign in to comment.