Skip to content

Commit

Permalink
Merge pull request #2413 from exadel-inc/feat/esl-event-listener-update
Browse files Browse the repository at this point in the history
feat(esl-event-listener): update listener internal mechanics to store and collect descriptors (with ability to filter them)
  • Loading branch information
ala-n authored May 21, 2024
2 parents 7a06d33 + d487365 commit 215a601
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 59 deletions.
24 changes: 22 additions & 2 deletions src/modules/esl-event-listener/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,20 +309,40 @@ Predicate to check if the passed argument is a type of `ESLListenerDescriptorFn
ESLEventUtils.isEventDescriptor(obj: any): obj is ESLListenerDescriptorFn;
```

<a name="-esleventutilsdescriptors"></a>

### `ESLEventUtils.descriptors`

Gathers descriptors from the passed object.
Accepts criteria to filter the descriptors list.

```typescript
ESLEventUtils.descriptors(host?: any): ESLListenerDescriptorFn[];
ESLEventUtils.descriptors(host?: any, ...criteria: ESLListenerDescriptorCriteria[]): ESLListenerDescriptorFn[];
```

**Parameters**:

- `host` - object to get auto-collectable descriptors from;


<a name="-esleventutilsgetautodescriptors"></a>

### `ESLEventUtils.getAutoDescriptors`
### <strike>`ESLEventUtils.getAutoDescriptors`</strike>

Gathers auto-subscribable (collectable) descriptors from the passed object.

Deprecated: prefer using `ESLEventUtils.descriptors` with the `{auto: true}` criteria. As the `getAutoDescriptors` method is going to be removed in 6th release.

```typescript
ESLEventUtils.descriptors(host?: any): ESLListenerDescriptorFn[]
ESLEventUtils.getAutoDescriptors(host?: any): ESLListenerDescriptorFn[]
```

**Parameters**:

- `host` - object to get auto-collectable descriptors from;


<a name="-esleventutilsinitdescriptor"></a>

### `ESLEventUtils.initDescriptor`
Expand Down
13 changes: 10 additions & 3 deletions src/modules/esl-event-listener/core/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {ExportNs} from '../../esl-utils/environment/export-ns';
import {dispatchCustomEvent} from '../../esl-utils/dom/events/misc';

import {ESLEventListener} from './listener';
import {getAutoDescriptors, isEventDescriptor, initDescriptor} from './descriptors';
import {getDescriptors, isEventDescriptor, initDescriptor} from './descriptors';

import type {
ESLListenerHandler,
Expand All @@ -23,7 +23,14 @@ export class ESLEventUtils {
public static dispatch = dispatchCustomEvent;

/** Gets {@link ESLListenerDescriptorFn}s of the passed object */
public static getAutoDescriptors = getAutoDescriptors;
public static descriptors = getDescriptors;

/**
* Gets auto {@link ESLListenerDescriptorFn}s of the passed object
*
* @deprecated alias for `descriptors(host, {auto: true})`
*/
public static getAutoDescriptors = (host: object): ESLListenerDescriptorFn[] => getDescriptors(host, {auto: true});

/**
* Decorates passed `key` of the `host` as an {@link ESLListenerDescriptorFn} using `desc` meta information
Expand Down Expand Up @@ -83,7 +90,7 @@ export class ESLEventUtils {
handler: ESLListenerHandler = eventDesc as ESLListenerDescriptorFn
): ESLEventListener[] {
if (arguments.length === 1) {
const descriptors = getAutoDescriptors(host);
const descriptors = getDescriptors(host, {auto: true});
return descriptors.flatMap((desc) => ESLEventUtils.subscribe(host, desc));
}
const desc = typeof eventDesc === 'string' ? {event: eventDesc} : eventDesc as ESLListenerDescriptor;
Expand Down
31 changes: 25 additions & 6 deletions src/modules/esl-event-listener/core/descriptors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import {isObject} from '../../esl-utils/misc/object/types';
import {isSimilar} from '../../esl-utils/misc/object/compare';

import type {ESLListenerDescriptorExt, ESLListenerDescriptorFn} from './types';
import type {
ESLListenerDescriptorCriteria,
ESLListenerDescriptor,
ESLListenerDescriptorExt,
ESLListenerDescriptorFn
} from './types';

/** Key to store listeners on the host */
const DESCRIPTORS = (window.Symbol || String)('__esl_descriptors');
Expand Down Expand Up @@ -39,12 +45,25 @@ export function isEventDescriptor(obj: any): obj is ESLListenerDescriptorFn {
return typeof obj.event === 'string' || typeof obj.event === 'function';
}

/** Gets {@link ESLListenerDescriptorFn}s of the passed object */
export function getAutoDescriptors(host: object): ESLListenerDescriptorFn[] {
/** Checks if the descriptor (passed as a context) matches the criteria */
function isMatchesDescriptor(
this: ESLListenerDescriptor,
criteria: ESLListenerDescriptorCriteria
): boolean {
if (typeof criteria === 'string') return this.event === criteria;
if (typeof criteria === 'object') return isSimilar(this, criteria, false);
return false;
}

/** Gets {@link ESLListenerDescriptorFn}s of the passed object that matches passed criterias */
export function getDescriptors(
host: object,
...criteria: ESLListenerDescriptorCriteria[]
): ESLListenerDescriptorFn[] {
if (!isObject(host)) return [];
const keys = getDescriptorsKeysFor(host);
const values = keys.map((key) => host[key]);
return values.filter((desc: ESLListenerDescriptorFn) => isEventDescriptor(desc) && desc.auto === true);
const values = keys.map((key) => host[key]).filter(isEventDescriptor);
return values.filter((desc) => criteria.every(isMatchesDescriptor, Object.assign({}, desc)));
}

/**
Expand All @@ -71,6 +90,6 @@ export function initDescriptor<T extends object>(
desc = Object.assign({auto: false}, desc);
}

if (desc.auto) addDescriptorKey(host, key);
addDescriptorKey(host, key);
return Object.assign(fn, desc) as ESLListenerDescriptorFn;
}
1 change: 0 additions & 1 deletion src/modules/esl-event-listener/core/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ export class ESLEventListener implements ESLListenerDefinition, EventListenerObj
}
/** Adds a listener to the listener store of the host object */
protected static add(host: object, instance: ESLEventListener): void {
if (!isObjectLike(host)) return;
if (!Object.hasOwnProperty.call(host, LISTENERS)) (host as any)[LISTENERS] = [];
(host as any)[LISTENERS].push(instance);
}
Expand Down
6 changes: 4 additions & 2 deletions src/modules/esl-event-listener/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ export interface ESLListenerDefinition<EType extends keyof ESLListenerEventMap =
export type ESLListenerHandler<EType extends Event = Event> = ((event: EType) => void) | (() => void);

/** Condition (criteria) to find {@link ESLListenerDescriptor} */
export type ESLListenerCriteria =
export type ESLListenerDescriptorCriteria =
| undefined
| keyof ESLListenerEventMap
| ESLListenerHandler
| Partial<ESLListenerDefinition>;

/** Condition (criteria) to find {@link ESLEventListener} */
export type ESLListenerCriteria = ESLListenerDescriptorCriteria | ESLListenerHandler;

/** Function decorated as {@link ESLListenerDescriptor} */
export type ESLListenerDescriptorFn<EType extends keyof ESLListenerEventMap = string> =
ESLListenerHandler<ESLListenerEventMap[EType]> & ESLListenerDescriptor<EType>;
Expand Down
122 changes: 91 additions & 31 deletions src/modules/esl-event-listener/test/descriptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ describe('dom/events: ESLEventUtils: ESLListenerDescriptor Utils', () => {
const host = {fn: () => void 0};
const desc = ESLEventUtils.initDescriptor(host, 'fn', {event: 'event'});
expect(desc.auto).toBe(false);
expect(ESLEventUtils.getAutoDescriptors(host)).not.toContain(desc);
expect(ESLEventUtils.descriptors(host, {auto: true})).not.toContain(desc);
});

test('ESLEventUtils.initDescriptor: auto=true makes descriptor auto-subscribable', () => {
const host = {fn: () => void 0};
const desc = ESLEventUtils.initDescriptor(host, 'fn', {event: 'event', auto: true});
expect(desc.auto).toBe(true);
expect(ESLEventUtils.getAutoDescriptors(host)).toContain(desc);
expect(ESLEventUtils.descriptors(host, {auto: true})).toContain(desc);
});
});

Expand Down Expand Up @@ -110,72 +110,132 @@ describe('dom/events: ESLEventUtils: ESLListenerDescriptor Utils', () => {
const desc = ESLEventUtils.initDescriptor(host, 'fn', {inherit: true});
expect(parentDesc.auto).toBe(true);
expect(desc.auto).toBe(true);
expect(ESLEventUtils.getAutoDescriptors(host)).toContain(desc);
expect(ESLEventUtils.descriptors(host, {auto: true})).toContain(desc);
});

test('ESLEventUtils.descriptors({auto: true}): does not catch prototype-level declared manual descriptor', () => {
class Test {
onEvent() {}
}
ESLEventUtils.initDescriptor(Test.prototype, 'onEvent', {event: 'event'});

const instance = new Test();
expect(ESLEventUtils.descriptors(instance, {auto: true})).toEqual([]);
});
});
});

describe('ESLEventUtils.getAutoDescriptors', () => {
describe('ESLEventUtils.getAutoDescriptors: empty cases', () => {
describe('ESLEventUtils.descriptors', () => {
describe('ESLEventUtils.descriptors: empty cases', () => {
test.each([
[undefined],
[null],
[''],
[{}],
[{event: ''}],
[{onEvent() {}}],
[new (class Test {onEvent() {}})()]
])('host = %p', (host: any) => expect(ESLEventUtils.getAutoDescriptors(host)).toEqual([]));
[new (class Test {
onEvent() {}
})()]
])('host = %p', (host: any) => expect(ESLEventUtils.descriptors(host)).toEqual([]));
});

test('ESLEventUtils.getAutoDescriptors: catch prototype-level declared descriptor', () => {
class Test { onEvent() {}}
ESLEventUtils.initDescriptor(Test.prototype, 'onEvent', {event: 'event', auto: true});
test('ESLEventUtils.descriptors: catch prototype-level declared descriptor', () => {
class Test {onEvent() {}}
ESLEventUtils.initDescriptor(Test.prototype, 'onEvent', {event: 'event'});

const instance = new Test();
expect(ESLEventUtils.getAutoDescriptors(instance)).toEqual([Test.prototype.onEvent]);
expect(ESLEventUtils.descriptors(instance)).toEqual([Test.prototype.onEvent]);
});

test('ESLEventUtils.getAutoDescriptors: does not catch prototype-level declared manual descriptor', () => {
class Test { onEvent() {}}
ESLEventUtils.initDescriptor(Test.prototype, 'onEvent', {event: 'event', auto: false});
describe('ESLEventUtils.descriptors: handles deep inheritance cases', () => {
class Base {
onEvent() {}
}
ESLEventUtils.initDescriptor(Base.prototype, 'onEvent', {event: 'event'});

const instance = new Test();
expect(ESLEventUtils.getAutoDescriptors(instance)).toEqual([]);
});

describe('ESLEventUtils.getAutoDescriptors: handles deep inheritance cases', () => {
class Base { onEvent() {}}
ESLEventUtils.initDescriptor(Base.prototype, 'onEvent', {event: 'event', auto: true});

test('ESLEventUtils.getAutoDescriptors: catch superclass level descriptor', () => {
test('ESLEventUtils.descriptors: catch superclass level descriptor', () => {
class Child extends Base {}
const instance = new Child();
expect(ESLEventUtils.getAutoDescriptors(instance)).toEqual([Base.prototype.onEvent]);
expect(ESLEventUtils.descriptors(instance)).toEqual([Base.prototype.onEvent]);
});

test('ESLEventUtils.getAutoDescriptors: simple override exclude descriptor from auto-subscription', () => {
test('ESLEventUtils.descriptors: simple override exclude descriptor', () => {
class Child extends Base {
public override onEvent() {}
}
const instance = new Child();
expect(ESLEventUtils.getAutoDescriptors(instance)).toEqual([]);
expect(ESLEventUtils.descriptors(instance)).toEqual([]);
});

test('ESLEventUtils.getAutoDescriptors: override with descriptor declaration consumes overridden descriptor', () => {
test('ESLEventUtils.descriptors: override with descriptor declaration consumes overridden descriptor', () => {
class Child extends Base {
public override onEvent() {}
}
ESLEventUtils.initDescriptor(Child.prototype, 'onEvent', {inherit: true});
const instance = new Child();
expect(ESLEventUtils.getAutoDescriptors(instance)).toEqual([Child.prototype.onEvent]);
expect(ESLEventUtils.descriptors(instance)).toEqual([Child.prototype.onEvent]);
});
});

test('ESLEventUtils.getAutoDescriptors: low level API consumes own properties as well', () => {
test('ESLEventUtils.descriptors: low level API consumes own properties as well', () => {
const obj = {onEvent: () => void 0};
ESLEventUtils.initDescriptor(obj, 'onEvent', {event: 'event', auto: true});
expect(ESLEventUtils.getAutoDescriptors(obj)).toEqual([obj.onEvent]);
ESLEventUtils.initDescriptor(obj, 'onEvent', {event: 'event'});
expect(ESLEventUtils.descriptors(obj)).toEqual([obj.onEvent]);
});

describe('ESLEventUtils.descriptors: filters by criteria', () => {
class Test {
onEvent() {}
onEventWithGroup() {}
onEvent2WithGroup() {}
onEventAuto() {}
}
ESLEventUtils.initDescriptor(Test.prototype, 'onEvent', {event: 'event'});
ESLEventUtils.initDescriptor(Test.prototype, 'onEventWithGroup', {event: 'event', group: 'group'});
ESLEventUtils.initDescriptor(Test.prototype, 'onEvent2WithGroup', {event: 'event2', group: 'group'});
ESLEventUtils.initDescriptor(Test.prototype, 'onEventAuto', {event: 'event', auto: true});

test('ESLEventUtils.descriptors: filters by event name', () => {
const instance = new Test();
expect(ESLEventUtils.descriptors(instance, 'event')).toEqual([
Test.prototype.onEvent,
Test.prototype.onEventWithGroup,
Test.prototype.onEventAuto
]);
});

test('ESLEventUtils.descriptors: filters by group name', () => {
const instance = new Test();
expect(ESLEventUtils.descriptors(instance, {group: 'group'})).toEqual([
Test.prototype.onEventWithGroup,
Test.prototype.onEvent2WithGroup
]);
});

test('ESLEventUtils.descriptors: filters by auto:true criteria', () => {
const instance = new Test();
expect(ESLEventUtils.descriptors(instance, {auto: true})).toEqual([Test.prototype.onEventAuto]);
});

test('ESLEventUtils.descriptors: filters by auto:false criteria', () => {
const instance = new Test();
expect(ESLEventUtils.descriptors(instance, {auto: false})).toEqual([
Test.prototype.onEvent,
Test.prototype.onEventWithGroup,
Test.prototype.onEvent2WithGroup
]);
});

test('ESLEventUtils.descriptors: filters by event name and group name', () => {
const instance = new Test();
expect(ESLEventUtils.descriptors(instance, {event: 'event', group: 'group'})).toEqual([Test.prototype.onEventWithGroup]);
});

test('ESLEventUtils.descriptors: filters by multiple criteria', () => {
const instance = new Test();
expect(ESLEventUtils.descriptors(instance, 'event', {group: 'group'})).toEqual([Test.prototype.onEventWithGroup]);
});
});
});
});
Loading

0 comments on commit 215a601

Please sign in to comment.