Skip to content

Commit

Permalink
feat(esl-event-listener): add the ability to get the current delegate…
Browse files Browse the repository at this point in the history
…d event target (#1675)
  • Loading branch information
abarmina committed Jun 9, 2023
1 parent e44b181 commit 8b4b089
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 7 deletions.
21 changes: 21 additions & 0 deletions src/modules/esl-event-listener/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,27 @@ Here is the list of supported keys of `ESLEventDesriptor`:
<u>Default Value:</u> `null`
<u>Description:</u> 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<EventType>` 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<MouseEvent>) {
const {$delegate} = e;
...
}
```

Supports `PropertyProvider` to declare the computed value as well.

- #### `capture` key
Expand Down
17 changes: 10 additions & 7 deletions src/modules/esl-event-listener/core/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}));
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/modules/esl-event-listener/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ declare global {
}
}

/** Event containing a delegated event target */
export type DelegatedEvent<EventType extends Event> = 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;

Expand Down
14 changes: 14 additions & 0 deletions src/modules/esl-event-listener/test/listener.delegate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}));
});
});
23 changes: 23 additions & 0 deletions src/modules/esl-utils/decorators/test/listen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<MouseEvent>) {
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();
});
});

0 comments on commit 8b4b089

Please sign in to comment.