Skip to content

Commit

Permalink
feat(common): add group name to bind and unbindAll methods (#1150)
Browse files Browse the repository at this point in the history
- in order to bind and unbind a set of event listeners more easily, we can add an optional group name, for example this will be helpful to unbind all event related to sub-menus while still keeping other event listeners in place
  • Loading branch information
ghiscoding committed Oct 25, 2023
1 parent 723e11c commit 6c3b90e
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 20 deletions.
Expand Up @@ -2,4 +2,5 @@ export interface ElementEventListener {
element: Element;
eventName: string;
listener: EventListenerOrEventListenerObject;
groupName?: string;
}
Expand Up @@ -44,7 +44,6 @@ describe('BindingEvent Service', () => {
});

it('should be able to bind an event with single listener and options to multiple elements', () => {
const mockElm = { addEventListener: jest.fn() } as unknown as HTMLElement;
const mockCallback = jest.fn();
const elm1 = document.createElement('input');
const elm2 = document.createElement('input');
Expand All @@ -65,8 +64,6 @@ describe('BindingEvent Service', () => {

it('should call unbindAll and expect as many removeEventListener be called', () => {
const mockElm = { addEventListener: jest.fn(), removeEventListener: jest.fn() } as unknown as HTMLElement;
const addEventSpy = jest.spyOn(mockElm, 'addEventListener');
const removeEventSpy = jest.spyOn(mockElm, 'removeEventListener');
const mockCallback1 = jest.fn();
const mockCallback2 = jest.fn();

Expand All @@ -78,9 +75,42 @@ describe('BindingEvent Service', () => {
service.unbindAll();

expect(service.boundedEvents.length).toBe(0);
expect(addEventSpy).toHaveBeenCalledWith('keyup', mockCallback1, undefined);
expect(addEventSpy).toHaveBeenCalledWith('click', mockCallback2, { capture: true, passive: true });
expect(removeEventSpy).toHaveBeenCalledWith('keyup', mockCallback1);
expect(removeEventSpy).toHaveBeenCalledWith('click', mockCallback2);
expect(mockElm.addEventListener).toHaveBeenCalledWith('keyup', mockCallback1, undefined);
expect(mockElm.addEventListener).toHaveBeenCalledWith('click', mockCallback2, { capture: true, passive: true });
expect(mockElm.removeEventListener).toHaveBeenCalledWith('keyup', mockCallback1);
expect(mockElm.removeEventListener).toHaveBeenCalledWith('click', mockCallback2);
});

it('should call unbindAll with a group name and expect that group listeners to be removed but others kept', () => {
const mockElm1 = { addEventListener: jest.fn(), removeEventListener: jest.fn() } as unknown as HTMLElement;
const mockElm2 = { addEventListener: jest.fn(), removeEventListener: jest.fn() } as unknown as HTMLElement;
const mockCallback1 = jest.fn();
const mockCallback2 = jest.fn();
const mockCallback3 = jest.fn();
const mockCallback4 = jest.fn();
const mockCallback5 = jest.fn();

service = new BindingEventService();
service.bind(mockElm1, 'keyup', mockCallback1, false, 'same-group');
service.bind(mockElm1, 'keydown', mockCallback2, { capture: true, passive: true }, 'magic');
service.bind(mockElm2, 'click', mockCallback3, { capture: true, passive: true }); // no group
service.bind(mockElm2, 'mouseover', mockCallback4, { capture: false, passive: true }, 'same-group');
service.bind(mockElm2, 'mouseout', mockCallback5, { capture: false, passive: false }, 'wonderful');

expect(service.boundedEvents.length).toBe(5);
expect(mockElm1.addEventListener).toHaveBeenCalledWith('keyup', mockCallback1, false); // same-group
expect(mockElm1.addEventListener).toHaveBeenCalledWith('keydown', mockCallback2, { capture: true, passive: true });
expect(mockElm2.addEventListener).toHaveBeenCalledWith('click', mockCallback3, { capture: true, passive: true });
expect(mockElm2.addEventListener).toHaveBeenCalledWith('mouseover', mockCallback4, { capture: false, passive: true }); // same-group
expect(mockElm2.addEventListener).toHaveBeenCalledWith('mouseout', mockCallback5, { capture: false, passive: false });

service.unbindAll('same-group');

expect(service.boundedEvents.length).toBe(3);
expect(mockElm1.removeEventListener).toHaveBeenCalledWith('keyup', mockCallback1); // same-group
expect(mockElm1.removeEventListener).not.toHaveBeenCalledWith();
expect(mockElm2.removeEventListener).not.toHaveBeenCalledWith();
expect(mockElm2.removeEventListener).toHaveBeenCalledWith('mouseover', mockCallback4); // same-group
expect(mockElm2.removeEventListener).not.toHaveBeenCalledWith();
});
});
45 changes: 32 additions & 13 deletions packages/common/src/services/bindingEvent.service.ts
Expand Up @@ -13,43 +13,62 @@ export class BindingEventService {
}

/** Bind an event listener to any element */
bind(elementOrElements: Element | NodeListOf<Element> | Window, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) {
bind(elementOrElements: Element | NodeListOf<Element> | Window, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject, listenerOptions?: boolean | AddEventListenerOptions, groupName = '') {
// convert to array for looping in next task
const eventNames = (Array.isArray(eventNameOrNames)) ? eventNameOrNames : [eventNameOrNames];

if ((elementOrElements as NodeListOf<HTMLElement>)?.forEach) {
(elementOrElements as NodeListOf<HTMLElement>)?.forEach(element => {
// multiple elements to bind to
(elementOrElements as NodeListOf<HTMLElement>).forEach(element => {
for (const eventName of eventNames) {
element.addEventListener(eventName, listener, options);
element.addEventListener(eventName, listener, listenerOptions);
this._boundedEvents.push({ element, eventName, listener });
}
});
} else {
// single elements to bind to
for (const eventName of eventNames) {
(elementOrElements as Element).addEventListener(eventName, listener, options);
this._boundedEvents.push({ element: (elementOrElements as Element), eventName, listener });
(elementOrElements as Element).addEventListener(eventName, listener, listenerOptions);
this._boundedEvents.push({ element: (elementOrElements as Element), eventName, listener, groupName });
}
}
}

/** Unbind all will remove every every event handlers that were bounded earlier */
/** Unbind a specific listener that was bounded earlier */
unbind(elementOrElements: Element | NodeListOf<Element>, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject) {
// convert to array for looping in next task
const elements = (Array.isArray(elementOrElements)) ? elementOrElements : [elementOrElements];
const eventNames = Array.isArray(eventNameOrNames) ? eventNameOrNames : [eventNameOrNames];

for (const eventName of eventNames) {
for (const element of elements) {
if (element?.removeEventListener) {
if (typeof element?.removeEventListener === 'function') {
element.removeEventListener(eventName, listener);
}
}
}
}

/** Unbind all will remove every every event handlers that were bounded earlier */
unbindAll() {
while (this._boundedEvents.length > 0) {
const boundedEvent = this._boundedEvents.pop() as ElementEventListener;
const { element, eventName, listener } = boundedEvent;
this.unbind(element, eventName, listener);
/**
* Unbind all event listeners that were bounded, optionally provide a group name to unbind all listeners assigned to that specific group only.
*/
unbindAll(groupName?: string) {
if (groupName) {
// unbind only the bounded event with a specific group
this._boundedEvents.forEach((boundedEvent, idx) => {
if (boundedEvent.groupName === groupName) {
const { element, eventName, listener } = boundedEvent;
this.unbind(element, eventName, listener);
this._boundedEvents.splice(idx, 1);
}
});
} else {
// unbind everything
while (this._boundedEvents.length > 0) {
const boundedEvent = this._boundedEvents.pop() as ElementEventListener;
const { element, eventName, listener } = boundedEvent;
this.unbind(element, eventName, listener);
}
}
}
}

0 comments on commit 6c3b90e

Please sign in to comment.