Repository: exadel-inc/esl
Pull request #1238 (2022-09-23 13:24:12)

refactor(esl-event-listener): update descriptors related methods


Patch:
diff --git a/src/modules/esl-event-listener/core.ts b/src/modules/esl-event-listener/core.ts
index 6b9a07edb..3253e46de 100644
--- a/src/modules/esl-event-listener/core.ts
+++ b/src/modules/esl-event-listener/core.ts
@@ -7,7 +7,7 @@ export type {
   ESLListenerDescriptorFn,
   ESLListenerTarget,
   ESLListenerDefinition
-} from './core/descriptor';
+} from './core/types';
 export type {
   ESLEventListener,
 } from './core/listener';
diff --git a/src/modules/esl-event-listener/core/api.ts b/src/modules/esl-event-listener/core/api.ts
index 2837101f6..7522ec367 100644
--- a/src/modules/esl-event-listener/core/api.ts
+++ b/src/modules/esl-event-listener/core/api.ts
@@ -1,5 +1,6 @@
 import {ExportNs} from '../../esl-utils/environment/export-ns';
 import {dispatchCustomEvent} from '../../esl-utils/dom/events/misc';
+import {extractValues} from '../../esl-utils/misc/object';
 import {ESLEventListener} from './listener';
 
 import type {
@@ -8,11 +9,8 @@ import type {
   ESLListenerDescriptor,
   ESLListenerEventMap,
   ESLListenerDescriptorFn
-} from './descriptor';
+} from './types';
 
-/** Type guard to check if the passed function is typeof {@link ESLListenerDescriptorFn} */
-export const isDescriptorFn = (obj: any): obj is ESLListenerDescriptorFn =>
-  typeof obj === 'function' && Object.hasOwnProperty.call(obj, 'event');
 
 @ExportNs('EventUtils')
 export class EventUtils {
@@ -24,14 +22,14 @@ export class EventUtils {
     return dispatchCustomEvent(el, eventName, eventInit);
   }
 
-  /** Gets descriptors from the passed object */
-  public static descriptors(target?: any, auto: boolean = true): ESLListenerDescriptorFn[] {
-    if (!target) return [];
-    const desc: ESLListenerDescriptorFn[] = [];
-    for (const key in target) {
-      if (isDescriptorFn(target[key]) && target[key].auto === auto) desc.push(target[key]);
-    }
-    return desc;
+  /** Type guard to check if the passed function is typeof {@link ESLListenerDescriptorFn} */
+  public static isEventDescriptor(obj: any): obj is ESLListenerDescriptorFn {
+    return typeof obj === 'function' && Object.hasOwnProperty.call(obj, 'event');
+  }
+
+  /** Gets {@link ESLListenerDescriptorFn}s of the passed object */
+  public static descriptors(target: any, auto: boolean = true): ESLListenerDescriptorFn[] {
+    return extractValues(target, (value) => EventUtils.isEventDescriptor(value) && value.auto === auto);
   }
 
   /**
@@ -64,7 +62,7 @@ export class EventUtils {
   ): ESLEventListener[] {
     if (typeof handler !== 'function') return [];
     if (typeof eventDesc === 'string') eventDesc = {event: eventDesc};
-    if (isDescriptorFn(handler) && eventDesc !== handler) eventDesc = Object.assign({}, handler, eventDesc);
+    if (EventUtils.isEventDescriptor(handler) && eventDesc !== handler) eventDesc = Object.assign({}, handler, eventDesc);
 
     const listeners = ESLEventListener.createOrResolve(host, handler, eventDesc as ESLListenerDescriptor);
     const subscribed = listeners.filter((listener) => listener.subscribe());
diff --git a/src/modules/esl-event-listener/core/listener.ts b/src/modules/esl-event-listener/core/listener.ts
index 12989db8b..6f44cad0e 100644
--- a/src/modules/esl-event-listener/core/listener.ts
+++ b/src/modules/esl-event-listener/core/listener.ts
@@ -12,7 +12,7 @@ import type {
   ESLListenerDescriptor,
   ESLListenerHandler,
   ESLListenerCriteria
-} from './descriptor';
+} from './types';
 
 /** Key to store listeners on the host */
 const STORE = '__esl_listeners';
diff --git a/src/modules/esl-event-listener/core/descriptor.ts b/src/modules/esl-event-listener/core/types.ts
similarity index 100%
rename from src/modules/esl-event-listener/core/descriptor.ts
rename to src/modules/esl-event-listener/core/types.ts
diff --git a/src/modules/esl-utils/decorators/listen.ts b/src/modules/esl-utils/decorators/listen.ts
index 2c03bf4c2..e4d0ee911 100644
--- a/src/modules/esl-utils/decorators/listen.ts
+++ b/src/modules/esl-utils/decorators/listen.ts
@@ -1,4 +1,4 @@
-import {isDescriptorFn} from '../dom/events';
+import {EventUtils} from '../../esl-event-listener/core';
 
 import type {PropertyProvider} from '../misc/functions';
 import type {ESLListenerHandler, ESLListenerEventMap, ESLListenerDescriptor} from '../dom/events';
@@ -30,13 +30,13 @@ export function listen(desc: string | ESLListenerDescriptorExt): ListenDecorator
                                                          descriptor: TypedPropertyDescriptor<T>): void {
     const superDesc = Object.getPrototypeOf(target)[propertyKey];
     desc = typeof desc === 'string' || typeof desc === 'function' ? {event: desc} : desc;
-    desc = Object.assign({auto: true}, desc.inherit && isDescriptorFn(superDesc) ? superDesc : {}, desc);
+    desc = Object.assign({auto: true}, desc.inherit && EventUtils.isEventDescriptor(superDesc) ? superDesc : {}, desc);
 
     const fn = descriptor.value || descriptor.get && descriptor.get.call(target);
     if (typeof fn !== 'function') {
       throw new TypeError('Only class methods can be decorated via listener decorator');
     }
-    if (isDescriptorFn(fn) && fn.event !== desc.event) {
+    if (EventUtils.isEventDescriptor(fn) && fn.event !== desc.event) {
       throw new TypeError(`Method ${propertyKey} already decorated as ESLListenerDescriptor`);
     }
 
diff --git a/src/modules/esl-utils/misc/object/utils.ts b/src/modules/esl-utils/misc/object/utils.ts
index 0c463c917..2255264ef 100644
--- a/src/modules/esl-utils/misc/object/utils.ts
+++ b/src/modules/esl-utils/misc/object/utils.ts
@@ -21,3 +21,15 @@ export function getPropertyDescriptor(o: any, prop: PropertyKey): PropertyDescri
     proto = Object.getPrototypeOf(proto);
   }
 }
+
+
+export function extractValues(obj: any, filter: (value: any, key: string | symbol) => boolean): any[];
+export function extractValues<T>(obj: any, filter: (value: any, key: string | symbol) => value is T): T[];
+export function extractValues(obj: any, filter: (value: any, key: string | symbol) => boolean): any[] {
+  if (!obj) return [];
+  const desc: any[] = [];
+  for (const key in obj) {
+    if (filter(obj[key], key)) desc.push(obj[key]);
+  }
+  return desc;
+}


Test patch:
diff --git a/src/modules/esl-utils/misc/object/test/utils.test.ts b/src/modules/esl-utils/misc/object/test/utils.test.ts
index 0fb5e1d66..1b0ab0a62 100644
--- a/src/modules/esl-utils/misc/object/test/utils.test.ts
+++ b/src/modules/esl-utils/misc/object/test/utils.test.ts
@@ -1,4 +1,4 @@
-import {defined, getPropertyDescriptor} from '../utils';
+import {defined, extractValues, getPropertyDescriptor} from '../utils';
 
 describe('misc/object: utils', () => {
   describe('getPropertyDescriptor', () => {
@@ -58,4 +58,54 @@ describe('misc/object: utils', () => {
       expect(defined(obj, null)).toBe(obj);
     });
   });
+
+  describe('extractValues', () => {
+    test('extractValues collect all values from simple object', () => {
+      const obj = {a: 1, b: 2};
+      expect(extractValues(obj, () => true)).toEqual(Object.values(obj));
+    });
+
+    test('extractValues collect all values from object prototype', () => {
+      const obj = {a: 1, b: 2};
+      const proto = {c: 3, d: 4};
+      Object.setPrototypeOf(obj, proto);
+      expect(extractValues(obj, () => true)).toEqual(Object.values(obj).concat(Object.values(proto)));
+    });
+
+    test('extractValues does not duplicate keys from prototype', () => {
+      const obj = {a: 1, b: 2};
+      const proto = {b: 4};
+      Object.setPrototypeOf(obj, proto);
+      expect(extractValues(obj, () => true)).toEqual(Object.values(obj));
+    });
+
+    test('extractValues calls filter function for each key', () => {
+      const obj = {a: 1, b: 2};
+      const proto = {d: 4};
+      Object.setPrototypeOf(obj, proto);
+      const filter = jest.fn(() => true);
+      extractValues(obj, filter);
+
+      expect(filter).toBeCalledTimes(3);
+      for (const pair of Object.entries(obj).concat(Object.entries(proto))) {
+        expect(filter).toHaveBeenCalledWith(...pair.reverse());
+      }
+    });
+
+    test('extractValues returns empty array with falsy filter', () => {
+      const obj = {a: 1, b: 2};
+      const proto = {b: 4};
+      Object.setPrototypeOf(obj, proto);
+      expect(extractValues(obj, () => false)).toEqual([]);
+    });
+
+    test('extractValues filter keys properly', () => {
+      const obj = {a: 1, b: 2};
+      const proto = {b: 4};
+      Object.setPrototypeOf(obj, proto);
+      const filter = jest.fn(() => false);
+      filter.mockImplementationOnce(() => true);
+      expect(extractValues(obj, filter)).toEqual([1]);
+    });
+  });
 });