From 8b4b089ca8d63ae7df0f39e4434ff7b2b38f5e59 Mon Sep 17 00:00:00 2001 From: Anna Date: Wed, 31 May 2023 13:28:18 +0200 Subject: [PATCH] feat(esl-event-listener): add the ability to get the current delegated event target (#1675) --- src/modules/esl-event-listener/README.md | 21 +++++++++++++++++ .../esl-event-listener/core/listener.ts | 17 ++++++++------ src/modules/esl-event-listener/core/types.ts | 5 ++++ .../test/listener.delegate.test.ts | 14 +++++++++++ .../esl-utils/decorators/test/listen.test.ts | 23 +++++++++++++++++++ 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/modules/esl-event-listener/README.md b/src/modules/esl-event-listener/README.md index 978bb4d6d..87451cbe2 100644 --- a/src/modules/esl-event-listener/README.md +++ b/src/modules/esl-event-listener/README.md @@ -111,6 +111,27 @@ Here is the list of supported keys of `ESLEventDesriptor`: Default Value: `null` Description: the CSS selector to filter event targets for event delegation mechanism. + ⚠ If you want to get the currently delegated event target, you can specify a new event type `DelegatedEvent` for function and then the `$delegate` variable will contain the desired selector element. + + So instead of this: + ```typescript + @listen({ event: 'click', selector: 'button' }) + onClick(e: Event) { + const {target} = e; + const $delegate = target && target.closest('button'); + ... + } + ``` + + You can use this: + ```typescript + @listen({ event: 'click', selector: 'button' }) + onClick(e: DelegatedEvent) { + const {$delegate} = e; + ... + } + ``` + Supports `PropertyProvider` to declare the computed value as well. - #### `capture` key diff --git a/src/modules/esl-event-listener/core/listener.ts b/src/modules/esl-event-listener/core/listener.ts index 229d18662..ea2015ccb 100644 --- a/src/modules/esl-event-listener/core/listener.ts +++ b/src/modules/esl-event-listener/core/listener.ts @@ -103,18 +103,21 @@ export class ESLEventListener implements ESLListenerDefinition, EventListenerObj ? (e: Event): void => (handlerBound(e), this.unsubscribe()) : handlerBound; return this.selector - ? (e: Event): void => this.isDelegatedTarget(e) && handlerFull(e) + ? (e: Event): void => this.handleDelegation(e, handlerFull) : handlerFull; } - /** Checks if the passed event can be handled by the current event listener */ - protected isDelegatedTarget(e: Event): boolean { + /** Executes handler if the passed event accepted by the selector */ + protected handleDelegation(e: Event, handler: EventListener): void { + const delegate = this.delegate; const target = e.target; const current = e.currentTarget; - const delegate = this.delegate; - if (typeof delegate !== 'string') return true; - if (!delegate || !(target instanceof Element) || !(current instanceof Element)) return false; - return current.contains(target.closest(delegate)); + + if (typeof delegate !== 'string' || !delegate) return; + if (!(target instanceof Element) || !(current instanceof Element)) return; + + const $delegate = target.closest(delegate); + if (current.contains($delegate)) handler(Object.assign(e, {$delegate})); } /** diff --git a/src/modules/esl-event-listener/core/types.ts b/src/modules/esl-event-listener/core/types.ts index 353d519d9..4c8eec267 100644 --- a/src/modules/esl-event-listener/core/types.ts +++ b/src/modules/esl-event-listener/core/types.ts @@ -8,6 +8,11 @@ declare global { } } +/** Event containing a delegated event target */ +export type DelegatedEvent = EventType & { + $delegate: Element | null; +}; + /** String CSS selector to find the target or {@link EventTarget} object or array of {@link EventTarget}s */ export type ESLListenerTarget = EventTarget | EventTarget[] | string | null; diff --git a/src/modules/esl-event-listener/test/listener.delegate.test.ts b/src/modules/esl-event-listener/test/listener.delegate.test.ts index d63cb134c..bbc832af3 100644 --- a/src/modules/esl-event-listener/test/listener.delegate.test.ts +++ b/src/modules/esl-event-listener/test/listener.delegate.test.ts @@ -64,4 +64,18 @@ describe('ESlEventListener subscription and delegation', () => { el.click(); expect(handler).toBeCalledTimes(1); }); + + test('Click on the target element leads to correct delegate information', () => { + const handler = jest.fn(); + ESLEventUtils.subscribe(host, {event: 'click', selector: '.btn'}, handler); + btn.click(); + expect(handler).toBeCalledWith(expect.objectContaining({$delegate: btn})); + }); + + test('Click inside the target element leads to correct delegate information', () => { + const handler = jest.fn(); + ESLEventUtils.subscribe(host, {event: 'click', selector: '.btn'}, handler); + btnSpan.click(); + expect(handler).toBeCalledWith(expect.objectContaining({$delegate: btn})); + }); }); diff --git a/src/modules/esl-utils/decorators/test/listen.test.ts b/src/modules/esl-utils/decorators/test/listen.test.ts index 155d21ab1..de2d9e5d1 100644 --- a/src/modules/esl-utils/decorators/test/listen.test.ts +++ b/src/modules/esl-utils/decorators/test/listen.test.ts @@ -4,6 +4,7 @@ import {listen} from '../listen'; import {ESLEventUtils} from '../../dom/events'; import type {ESLListenerDescriptorFn} from '../../dom/events'; +import type {DelegatedEvent} from '../../../esl-event-listener/core/types'; describe('Decorator: @listen', () => { test('Decorator listen should accept one argument call with an event type', () => { @@ -166,4 +167,26 @@ describe('Decorator: @listen', () => { expect(ESLEventUtils.getAutoDescriptors(test)).not.toContain(TestChild.prototype.onEventManual); }); }); + + test('@listen has additional information about delegated event target', () => { + class Test extends HTMLElement { + connectedCallback() { + const button = document.createElement('button'); + this.appendChild(button); + ESLEventUtils.subscribe(this); + } + + @listen({event: 'click', selector: 'button'}) + onSomeEvent(e: DelegatedEvent) { + expect(e.$delegate).toBeInstanceOf(Element); + } + } + + customElements.define('test-listen-selected-target', Test); + const test = new Test(); + document.body.appendChild(test); + + const button = test.querySelector('button'); + button?.click(); + }); });