From aed9c72696720c718d34e4bc0de707e514652ea5 Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Wed, 30 Aug 2023 15:10:23 +1000 Subject: [PATCH] feat: introduce `AbstractEvent` and upgrade to node 20.x --- .eslintrc | 1 + README.md | 73 ++++++ docs/assets/highlight.css | 48 +++- docs/assets/search.js | 2 +- docs/classes/AbstractEvent.html | 419 ++++++++++++++++++++++++++++++++ docs/classes/EventAny.html | 170 +++++++------ docs/functions/Evented-1.html | 3 +- docs/index.html | 36 ++- docs/interfaces/Evented.html | 6 +- docs/modules.html | 4 +- package-lock.json | 3 + package.json | 3 + pkgs.nix | 2 +- shell.nix | 2 +- src/AbstractEvent.ts | 52 ++++ src/EventAny.ts | 11 +- src/Evented.ts | 2 +- src/index.ts | 3 +- tests/index.test.ts | 45 +++- 19 files changed, 781 insertions(+), 104 deletions(-) create mode 100644 docs/classes/AbstractEvent.html create mode 100644 src/AbstractEvent.ts diff --git a/.eslintrc b/.eslintrc index 44a8d5a..786b7e5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -39,6 +39,7 @@ "message": "Use `globalThis` instead" } ], + "prefer-rest-params": 0, "require-yield": 0, "eqeqeq": ["error", "smart"], "spaced-comment": [ diff --git a/README.md b/README.md index bb85e97..c989444 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,79 @@ master:[![pipeline status](https://gitlab.com/MatrixAI/open-source/js-events/bad Events for push-flow abstractions. +### `AbstractEvent` + +```ts +// For when you just want a regular event without `detail` +// Note that the `detail` type is `null` +class Event1 extends AbstractEvent {} + +// For when you want a event with `detail` +class Event2 extends AbstractEvent {} + +// Allow caller to customise the `detail` type +// Note that the `detail` type is `unknown` +// This would be rare to use, prefer `Event4` +class Event3 extends AbstractEvent {} + +// Allow caller to customise the `detail` type +// But this is more accurate as not passing anything +// Would mean the `detail` is in fact `null` +class Event4 extends AbstractEvent {} + +// When you need to customise the constructor signature +class Event5 extends AbstractEvent { + constructor(options: CustomEventInit) { + // Make sure you pass `arguments`! + super(Event5.name, options, arguments); + } +} +``` + +When redispatching an event, you must call `event.clone()`. The same instance cannot be redispatched. When the event is cloned, all constructor parameters are shallow-copied. + +### `Evented` + +We combine `Evented` with `AbstractEvent` to gain type-safety and convenience of the wildcard any handler. + +```ts +class EventCustom extends AbstractEvent {} + +interface X extends Evented {} +@Evented() +class X {} + +const x = new X(); + +// Handle specific event, use the `name` property as the key +x.addEventListener(EventCustom.name, (e) => { + console.log(e as EventCustom); +}); + +// Handle any event +x.addEventListener((e) => { + // This is the wrapped underlying event + console.log((e as EventAny).detail); +}) +``` + +Note that all events pass through the any event handler, it is not a "fall through" handler. + +You can use this style to handle relevant events to perform side-effects, as well as propagate upwards irrelevant events. + +Note that some side-effects you perform may trigger an infinite loop by causing something to emit the specific event type that you are handling. In these cases you should specialise handling of those events with a `once: true` option, so that they are only handled once. + +```ts +x.addEventListener(EventInfinite.name, (e) => { + console.log(e as EventInfinite); + performActionThatMayTriggerEventInfinite(); +}, { once: true }); +``` + +This will terminate the infinite loop on the first time it gets handled. + +Therefore it is a good idea to always be as specific with your event types as possible. + ## Installation ```sh diff --git a/docs/assets/highlight.css b/docs/assets/highlight.css index 262bff7..82d92c0 100644 --- a/docs/assets/highlight.css +++ b/docs/assets/highlight.css @@ -1,14 +1,22 @@ :root { - --light-hl-0: #795E26; - --dark-hl-0: #DCDCAA; - --light-hl-1: #000000; - --dark-hl-1: #D4D4D4; - --light-hl-2: #A31515; - --dark-hl-2: #CE9178; - --light-hl-3: #0000FF; - --dark-hl-3: #569CD6; - --light-hl-4: #008000; - --dark-hl-4: #6A9955; + --light-hl-0: #008000; + --dark-hl-0: #6A9955; + --light-hl-1: #0000FF; + --dark-hl-1: #569CD6; + --light-hl-2: #000000; + --dark-hl-2: #D4D4D4; + --light-hl-3: #267F99; + --dark-hl-3: #4EC9B0; + --light-hl-4: #001080; + --dark-hl-4: #9CDCFE; + --light-hl-5: #795E26; + --dark-hl-5: #DCDCAA; + --light-hl-6: #0070C1; + --dark-hl-6: #4FC1FF; + --light-hl-7: #AF00DB; + --dark-hl-7: #C586C0; + --light-hl-8: #A31515; + --dark-hl-8: #CE9178; --light-code-background: #FFFFFF; --dark-code-background: #1E1E1E; } @@ -19,6 +27,10 @@ --hl-2: var(--light-hl-2); --hl-3: var(--light-hl-3); --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --hl-8: var(--light-hl-8); --code-background: var(--light-code-background); } } @@ -28,6 +40,10 @@ --hl-2: var(--dark-hl-2); --hl-3: var(--dark-hl-3); --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --hl-8: var(--dark-hl-8); --code-background: var(--dark-code-background); } } @@ -37,6 +53,10 @@ --hl-2: var(--light-hl-2); --hl-3: var(--light-hl-3); --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --hl-8: var(--light-hl-8); --code-background: var(--light-code-background); } @@ -46,6 +66,10 @@ --hl-2: var(--dark-hl-2); --hl-3: var(--dark-hl-3); --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --hl-8: var(--dark-hl-8); --code-background: var(--dark-code-background); } @@ -54,4 +78,8 @@ .hl-2 { color: var(--hl-2); } .hl-3 { color: var(--hl-3); } .hl-4 { color: var(--hl-4); } +.hl-5 { color: var(--hl-5); } +.hl-6 { color: var(--hl-6); } +.hl-7 { color: var(--hl-7); } +.hl-8 { color: var(--hl-8); } pre, code { background: var(--code-background); } diff --git a/docs/assets/search.js b/docs/assets/search.js index 2043b04..8326ac1 100644 --- a/docs/assets/search.js +++ b/docs/assets/search.js @@ -1 +1 @@ -window.searchData = JSON.parse("{\"kinds\":{\"4\":\"Namespace\",\"32\":\"Variable\",\"64\":\"Function\",\"128\":\"Class\",\"256\":\"Interface\",\"512\":\"Constructor\",\"1024\":\"Property\",\"2048\":\"Method\",\"65536\":\"Type literal\"},\"rows\":[{\"kind\":64,\"name\":\"Evented\",\"url\":\"functions/Evented-1.html\",\"classes\":\"tsd-kind-function\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"functions/Evented-1.html#Evented.__type\",\"classes\":\"tsd-kind-type-literal\",\"parent\":\"Evented.Evented\"},{\"kind\":256,\"name\":\"Evented\",\"url\":\"interfaces/Evented.html\",\"classes\":\"tsd-kind-interface\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"interfaces/Evented.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-interface\",\"parent\":\"Evented\"},{\"kind\":2048,\"name\":\"addEventListener\",\"url\":\"interfaces/Evented.html#addEventListener\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Evented\"},{\"kind\":2048,\"name\":\"dispatchEvent\",\"url\":\"interfaces/Evented.html#dispatchEvent\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Evented\"},{\"kind\":2048,\"name\":\"removeEventListener\",\"url\":\"interfaces/Evented.html#removeEventListener\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Evented\"},{\"kind\":128,\"name\":\"EventAny\",\"url\":\"classes/EventAny.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":1024,\"name\":\"type\",\"url\":\"classes/EventAny.html#type-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"EventAny\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/EventAny.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"EventAny\"},{\"kind\":1024,\"name\":\"detail\",\"url\":\"classes/EventAny.html#detail\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"EventAny\"},{\"kind\":4,\"name\":\"utils\",\"url\":\"modules/utils.html\",\"classes\":\"tsd-kind-namespace\"},{\"kind\":32,\"name\":\"_eventTarget\",\"url\":\"variables/utils._eventTarget.html\",\"classes\":\"tsd-kind-variable tsd-parent-kind-namespace\",\"parent\":\"utils\"},{\"kind\":32,\"name\":\"eventTarget\",\"url\":\"variables/utils.eventTarget.html\",\"classes\":\"tsd-kind-variable tsd-parent-kind-namespace\",\"parent\":\"utils\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,17.918]],[\"comment/0\",[]],[\"name/1\",[1,23.026]],[\"comment/1\",[]],[\"name/2\",[0,17.918]],[\"comment/2\",[]],[\"name/3\",[2,17.918]],[\"comment/3\",[]],[\"name/4\",[3,23.026]],[\"comment/4\",[]],[\"name/5\",[4,23.026]],[\"comment/5\",[]],[\"name/6\",[5,23.026]],[\"comment/6\",[]],[\"name/7\",[6,23.026]],[\"comment/7\",[]],[\"name/8\",[7,23.026]],[\"comment/8\",[]],[\"name/9\",[2,17.918]],[\"comment/9\",[]],[\"name/10\",[8,23.026]],[\"comment/10\",[]],[\"name/11\",[9,23.026]],[\"comment/11\",[]],[\"name/12\",[10,23.026]],[\"comment/12\",[]],[\"name/13\",[11,23.026]],[\"comment/13\",[]]],\"invertedIndex\":[[\"__type\",{\"_index\":1,\"name\":{\"1\":{}},\"comment\":{}}],[\"_eventtarget\",{\"_index\":10,\"name\":{\"12\":{}},\"comment\":{}}],[\"addeventlistener\",{\"_index\":3,\"name\":{\"4\":{}},\"comment\":{}}],[\"constructor\",{\"_index\":2,\"name\":{\"3\":{},\"9\":{}},\"comment\":{}}],[\"detail\",{\"_index\":8,\"name\":{\"10\":{}},\"comment\":{}}],[\"dispatchevent\",{\"_index\":4,\"name\":{\"5\":{}},\"comment\":{}}],[\"eventany\",{\"_index\":6,\"name\":{\"7\":{}},\"comment\":{}}],[\"evented\",{\"_index\":0,\"name\":{\"0\":{},\"2\":{}},\"comment\":{}}],[\"eventtarget\",{\"_index\":11,\"name\":{\"13\":{}},\"comment\":{}}],[\"removeeventlistener\",{\"_index\":5,\"name\":{\"6\":{}},\"comment\":{}}],[\"type\",{\"_index\":7,\"name\":{\"8\":{}},\"comment\":{}}],[\"utils\",{\"_index\":9,\"name\":{\"11\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file +window.searchData = JSON.parse("{\"kinds\":{\"4\":\"Namespace\",\"32\":\"Variable\",\"64\":\"Function\",\"128\":\"Class\",\"256\":\"Interface\",\"512\":\"Constructor\",\"1024\":\"Property\",\"2048\":\"Method\",\"65536\":\"Type literal\"},\"rows\":[{\"kind\":128,\"name\":\"AbstractEvent\",\"url\":\"classes/AbstractEvent.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/AbstractEvent.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"AbstractEvent\"},{\"kind\":1024,\"name\":\"constructorParams\",\"url\":\"classes/AbstractEvent.html#constructorParams\",\"classes\":\"tsd-kind-property tsd-parent-kind-class tsd-is-protected\",\"parent\":\"AbstractEvent\"},{\"kind\":2048,\"name\":\"clone\",\"url\":\"classes/AbstractEvent.html#clone\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"AbstractEvent\"},{\"kind\":128,\"name\":\"EventAny\",\"url\":\"classes/EventAny.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":1024,\"name\":\"type\",\"url\":\"classes/EventAny.html#type-1\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"EventAny\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/EventAny.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"EventAny\"},{\"kind\":1024,\"name\":\"constructorParams\",\"url\":\"classes/EventAny.html#constructorParams\",\"classes\":\"tsd-kind-property tsd-parent-kind-class tsd-is-protected tsd-is-inherited\",\"parent\":\"EventAny\"},{\"kind\":2048,\"name\":\"clone\",\"url\":\"classes/EventAny.html#clone\",\"classes\":\"tsd-kind-method tsd-parent-kind-class tsd-is-inherited\",\"parent\":\"EventAny\"},{\"kind\":64,\"name\":\"Evented\",\"url\":\"functions/Evented-1.html\",\"classes\":\"tsd-kind-function\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"functions/Evented-1.html#Evented.__type\",\"classes\":\"tsd-kind-type-literal\",\"parent\":\"Evented.Evented\"},{\"kind\":256,\"name\":\"Evented\",\"url\":\"interfaces/Evented.html\",\"classes\":\"tsd-kind-interface\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"interfaces/Evented.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-interface\",\"parent\":\"Evented\"},{\"kind\":2048,\"name\":\"addEventListener\",\"url\":\"interfaces/Evented.html#addEventListener\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Evented\"},{\"kind\":2048,\"name\":\"removeEventListener\",\"url\":\"interfaces/Evented.html#removeEventListener\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Evented\"},{\"kind\":2048,\"name\":\"dispatchEvent\",\"url\":\"interfaces/Evented.html#dispatchEvent\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Evented\"},{\"kind\":4,\"name\":\"utils\",\"url\":\"modules/utils.html\",\"classes\":\"tsd-kind-namespace\"},{\"kind\":32,\"name\":\"_eventTarget\",\"url\":\"variables/utils._eventTarget.html\",\"classes\":\"tsd-kind-variable tsd-parent-kind-namespace\",\"parent\":\"utils\"},{\"kind\":32,\"name\":\"eventTarget\",\"url\":\"variables/utils.eventTarget.html\",\"classes\":\"tsd-kind-variable tsd-parent-kind-namespace\",\"parent\":\"utils\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,25.903]],[\"comment/0\",[]],[\"name/1\",[1,17.43]],[\"comment/1\",[]],[\"name/2\",[2,20.794]],[\"comment/2\",[]],[\"name/3\",[3,20.794]],[\"comment/3\",[]],[\"name/4\",[4,25.903]],[\"comment/4\",[]],[\"name/5\",[5,25.903]],[\"comment/5\",[]],[\"name/6\",[1,17.43]],[\"comment/6\",[]],[\"name/7\",[2,20.794]],[\"comment/7\",[]],[\"name/8\",[3,20.794]],[\"comment/8\",[]],[\"name/9\",[6,20.794]],[\"comment/9\",[]],[\"name/10\",[7,25.903]],[\"comment/10\",[]],[\"name/11\",[6,20.794]],[\"comment/11\",[]],[\"name/12\",[1,17.43]],[\"comment/12\",[]],[\"name/13\",[8,25.903]],[\"comment/13\",[]],[\"name/14\",[9,25.903]],[\"comment/14\",[]],[\"name/15\",[10,25.903]],[\"comment/15\",[]],[\"name/16\",[11,25.903]],[\"comment/16\",[]],[\"name/17\",[12,25.903]],[\"comment/17\",[]],[\"name/18\",[13,25.903]],[\"comment/18\",[]]],\"invertedIndex\":[[\"__type\",{\"_index\":7,\"name\":{\"10\":{}},\"comment\":{}}],[\"_eventtarget\",{\"_index\":12,\"name\":{\"17\":{}},\"comment\":{}}],[\"abstractevent\",{\"_index\":0,\"name\":{\"0\":{}},\"comment\":{}}],[\"addeventlistener\",{\"_index\":8,\"name\":{\"13\":{}},\"comment\":{}}],[\"clone\",{\"_index\":3,\"name\":{\"3\":{},\"8\":{}},\"comment\":{}}],[\"constructor\",{\"_index\":1,\"name\":{\"1\":{},\"6\":{},\"12\":{}},\"comment\":{}}],[\"constructorparams\",{\"_index\":2,\"name\":{\"2\":{},\"7\":{}},\"comment\":{}}],[\"dispatchevent\",{\"_index\":10,\"name\":{\"15\":{}},\"comment\":{}}],[\"eventany\",{\"_index\":4,\"name\":{\"4\":{}},\"comment\":{}}],[\"evented\",{\"_index\":6,\"name\":{\"9\":{},\"11\":{}},\"comment\":{}}],[\"eventtarget\",{\"_index\":13,\"name\":{\"18\":{}},\"comment\":{}}],[\"removeeventlistener\",{\"_index\":9,\"name\":{\"14\":{}},\"comment\":{}}],[\"type\",{\"_index\":5,\"name\":{\"5\":{}},\"comment\":{}}],[\"utils\",{\"_index\":11,\"name\":{\"16\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file diff --git a/docs/classes/AbstractEvent.html b/docs/classes/AbstractEvent.html new file mode 100644 index 0000000..48261a6 --- /dev/null +++ b/docs/classes/AbstractEvent.html @@ -0,0 +1,419 @@ +AbstractEvent | @matrixai/events
+
+ +
+
+
+
+ +

Class AbstractEvent<T>

+
+

By default the detail property of CustomEvent is null. +So the default type has to be null too.

+
+
+

Type Parameters

+
    +
  • +

    T = null

+
+

Hierarchy

+
    +
  • CustomEvent<T> +
+
+
+
+ +
+
+

Constructors

+
+ +
    + +
  • +
    +

    Type Parameters

    +
      +
    • +

      T = null

    +
    +

    Parameters

    +
      +
    • +
      Optional type: string
    • +
    • +
      Optional options: CustomEventInit<T>
    • +
    • +
      Optional constructorParams: IArguments
    +

    Returns AbstractEvent<T>

  • + +
  • +
    +

    Type Parameters

    +
      +
    • +

      T = null

    +
    +

    Parameters

    +
      +
    • +
      options: CustomEventInit<T>
    • +
    • +
      Optional constructorParams: IArguments
    +

    Returns AbstractEvent<T>

+
+

Properties

+
+ +
AT_TARGET: number
+
+ +
BUBBLING_PHASE: number
+
+ +
CAPTURING_PHASE: number
+
+ +
NONE: number
+
+ +
bubbles: boolean
+

Returns true or false depending on how event was initialized. True if event goes through its target's ancestors in reverse tree order, and false otherwise.

+
+
+ +
cancelBubble: boolean
+
+

Deprecated

+
+ +
cancelable: boolean
+

Returns true or false depending on how event was initialized. Its return value does not always carry meaning, but true can indicate that part of the operation during which event was dispatched, can be canceled by invoking the preventDefault() method.

+
+
+ +
composed: boolean
+

Returns true or false depending on how event was initialized. True if event invokes listeners past a ShadowRoot node that is the root of its target, and false otherwise.

+
+
+ +
constructorParams: IArguments
+
+ +
currentTarget: null | EventTarget
+

Returns the object whose event listener's callback is currently being invoked.

+
+
+ +
defaultPrevented: boolean
+

Returns true if preventDefault() was invoked successfully to indicate cancelation, and false otherwise.

+
+
+ +
detail: T
+

Returns any custom data event was created with. Typically used for synthetic events.

+
+
+ +
eventPhase: number
+

Returns the event's phase, which is one of NONE, CAPTURING_PHASE, AT_TARGET, and BUBBLING_PHASE.

+
+
+ +
isTrusted: boolean
+

Returns true if event was dispatched by the user agent, and false otherwise.

+
+
+ +
returnValue: boolean
+
+

Deprecated

+
+ +
srcElement: null | EventTarget
+
+

Deprecated

+
+ +
target: null | EventTarget
+

Returns the object to which event is dispatched (its target).

+
+
+ +
timeStamp: number
+

Returns the event's timestamp as the number of milliseconds measured relative to the time origin.

+
+
+ +
type: string
+

Returns the type of event, e.g. "click", "hashchange", or "submit".

+
+
+

Methods

+
+ +
    + +
  • +

    Events cannot be re-dispatched. This was probably to prevent infinite +loops. So instead of re-dispatching the same instance, we re-dispatch +a clone of the instance.

    +
    +

    Returns AbstractEvent<T>

+
+ +
    + +
  • +

    Returns the invocation target objects of event's path (objects on which listeners will be invoked), except for any nodes in shadow trees of which the shadow root's mode is "closed" that are not reachable from event's currentTarget.

    +
    +

    Returns EventTarget[]

+
+ +
    + +
  • +
    +

    Deprecated

    +
    +

    Parameters

    +
      +
    • +
      type: string
    • +
    • +
      Optional bubbles: boolean
    • +
    • +
      Optional cancelable: boolean
    • +
    • +
      Optional detail: T
    +

    Returns void

+
+ +
    + +
  • +
    +

    Deprecated

    +
    +

    Parameters

    +
      +
    • +
      type: string
    • +
    • +
      Optional bubbles: boolean
    • +
    • +
      Optional cancelable: boolean
    +

    Returns void

+
+ +
    + +
  • +

    If invoked when the cancelable attribute value is true, and while executing a listener for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled.

    +
    +

    Returns void

+
+ +
    + +
  • +

    Invoking this method prevents event from reaching any registered event listeners after the current one finishes running and, when dispatched in a tree, also prevents event from reaching any other objects.

    +
    +

    Returns void

+
+ +
    + +
  • +

    When dispatched in a tree, invoking this method prevents event from reaching any objects other than the current object.

    +
    +

    Returns void

+
+
+

Generated using TypeDoc

+
\ No newline at end of file diff --git a/docs/classes/EventAny.html b/docs/classes/EventAny.html index 9c4c846..d51aae8 100644 --- a/docs/classes/EventAny.html +++ b/docs/classes/EventAny.html @@ -14,15 +14,24 @@ -

Class EventAny

+

Class EventAny<T>

+
+

By default the detail property of CustomEvent is null. +So the default type has to be null too.

+
+
+

Type Parameters

+
    +
  • +

    T = Event

Hierarchy

+
  • Defined in src/EventAny.ts:5
  • @@ -42,9 +51,10 @@

    Properties

    cancelBubble cancelable composed +constructorParams currentTarget defaultPrevented -detail +detail eventPhase isTrusted returnValue @@ -52,15 +62,13 @@

    Properties

    target timeStamp type -AT_TARGET -BUBBLING_PHASE -CAPTURING_PHASE -NONE type

    Methods

    -
    composedPath +
    clone +composedPath +initCustomEvent initEvent preventDefault stopImmediatePropagation @@ -71,15 +79,20 @@

    Constructors

      - +
    • +
      +

      Type Parameters

      +
        +
      • +

        T = Event

      Parameters

      • -
        options: EventInit & {
            detail: Event;
        }
      -

      Returns EventAny

    +

    Returns EventAny<T>

    @@ -87,25 +100,25 @@

    Properties

    AT_TARGET: number
    BUBBLING_PHASE: number
    CAPTURING_PHASE: number
    NONE: number
    @@ -113,7 +126,7 @@
    @@ -121,7 +134,7 @@
    @@ -129,7 +142,7 @@
    @@ -137,15 +150,21 @@
    +
    + +
    constructorParams: IArguments
    currentTarget: null | EventTarget

    Returns the object whose event listener's callback is currently being invoked.

    @@ -153,20 +172,23 @@
    -
    - -
    detail: Event
    eventPhase: number

    Returns the event's phase, which is one of NONE, CAPTURING_PHASE, AT_TARGET, and BUBBLING_PHASE.

    @@ -174,7 +196,7 @@
    @@ -182,7 +204,7 @@
    @@ -190,7 +212,7 @@
    @@ -198,7 +220,7 @@
    @@ -206,7 +228,7 @@
    @@ -214,40 +236,29 @@
    -
    - -
    AT_TARGET: number
    -
    - -
    BUBBLING_PHASE: number
    -
    - -
    CAPTURING_PHASE: number
    -
    - -
    NONE: number
    type: string = ...
    +
  • Defined in src/EventAny.ts:6
  • Methods

    +
    + +
      + +
    • +

      Events cannot be re-dispatched. This was probably to prevent infinite +loops. So instead of re-dispatching the same instance, we re-dispatch +a clone of the instance.

      +
      +

      Returns EventAny<T>

    +
    + +
      + +
    • +
      +

      Deprecated

      +
      +

      Parameters

      +
        +
      • +
        type: string
      • +
      • +
        Optional bubbles: boolean
      • +
      • +
        Optional cancelable: boolean
      • +
      • +
        Optional detail: T
      +

      Returns void

      @@ -276,7 +309,7 @@
      Optional bubbles: Optional cancelable: boolean

    Returns void

    @@ -287,7 +320,7 @@
    @@ -298,7 +331,7 @@
    @@ -309,7 +342,7 @@
    diff --git a/docs/index.html b/docs/index.html index f08a856..7c511be 100644 --- a/docs/index.html +++ b/docs/index.html @@ -20,23 +20,44 @@

    js-events

    master:pipeline status

    Events for push-flow abstractions.

    + +

    AbstractEvent

    +
    +
    // For when you just want a regular event without `detail`
    // Note that the `detail` type is `null`
    class Event1 extends AbstractEvent {}

    // For when you want a event with `detail`
    class Event2 extends AbstractEvent<string> {}

    // Allow caller to customise the `detail` type
    // Note that the `detail` type is `unknown`
    // This would be rare to use, prefer `Event4`
    class Event3<T> extends AbstractEvent<T> {}

    // Allow caller to customise the `detail` type
    // But this is more accurate as not passing anything
    // Would mean the `detail` is in fact `null`
    class Event4<T = null> extends AbstractEvent<T> {}

    // When you need to customise the constructor signature
    class Event5 extends AbstractEvent<string> {
    constructor(options: CustomEventInit<string>) {
    // Make sure you pass `arguments`!
    super(Event5.name, options, arguments);
    }
    } +
    +

    When redispatching an event, you must call event.clone(). The same instance cannot be redispatched. When the event is cloned, all constructor parameters are shallow-copied.

    + + +

    Evented

    +
    +

    We combine Evented with AbstractEvent to gain type-safety and convenience of the wildcard any handler.

    +
    class EventCustom extends AbstractEvent {}

    interface X extends Evented {}
    @Evented()
    class X {}

    const x = new X();

    // Handle specific event, use the `name` property as the key
    x.addEventListener(EventCustom.name, (e) => {
    console.log(e as EventCustom);
    });

    // Handle any event
    x.addEventListener((e) => {
    // This is the wrapped underlying event
    console.log((e as EventAny).detail);
    }) +
    +

    Note that all events pass through the any event handler, it is not a "fall through" handler.

    +

    You can use this style to handle relevant events to perform side-effects, as well as propagate upwards irrelevant events.

    +

    Note that some side-effects you perform may trigger an infinite loop by causing something to emit the specific event type that you are handling. In these cases you should specialise handling of those events with a once: true option, so that they are only handled once.

    +
    x.addEventListener(EventInfinite.name, (e) => {
    console.log(e as EventInfinite);
    performActionThatMayTriggerEventInfinite();
    }, { once: true }); +
    +

    This will terminate the infinite loop on the first time it gets handled.

    +

    Therefore it is a good idea to always be as specific with your event types as possible.

    +

    Installation

    -
    npm install --save @matrixai/events
    +
    npm install --save @matrixai/events
     

    Development

    Run nix-shell, and once you're inside, you can use:

    -
    # install (or reinstall packages from package.json)
    npm install
    # build the dist
    npm run build
    # run the repl (this allows you to import from ./src)
    npm run ts-node
    # run the tests
    npm run test
    # lint the source code
    npm run lint
    # automatically fix the source
    npm run lintfix +
    # install (or reinstall packages from package.json)
    npm install
    # build the dist
    npm run build
    # run the repl (this allows you to import from ./src)
    npm run ts-node
    # run the tests
    npm run test
    # lint the source code
    npm run lint
    # automatically fix the source
    npm run lintfix

    Docs Generation

    -
    npm run docs
    +
    npm run docs
     

    See the docs at: https://matrixai.github.io/js-events/

    @@ -45,13 +66,13 @@

    Publishing

    Publishing is handled automatically by the staging pipeline.

    Prerelease:

    -
    # npm login
    npm version prepatch --preid alpha # premajor/preminor/prepatch
    git push --follow-tags +
    # npm login
    npm version prepatch --preid alpha # premajor/preminor/prepatch
    git push --follow-tags

    Release:

    -
    # npm login
    npm version patch # major/minor/patch
    git push --follow-tags +
    # npm login
    npm version patch # major/minor/patch
    git push --follow-tags

    Manually:

    -
    # npm login
    npm version patch # major/minor/patch
    npm run build
    npm publish --access public
    git push
    git push --tags +
    # npm login
    npm version patch # major/minor/patch
    npm run build
    npm publish --access public
    git push
    git push --tags
    diff --git a/docs/interfaces/Evented.html b/docs/interfaces/Evented.html index ad78707..922d990 100644 --- a/docs/interfaces/Evented.html +++ b/docs/interfaces/Evented.html @@ -85,7 +85,7 @@

    Parameters

    event: Event

    Returns boolean

    +
  • Defined in src/Evented.ts:23
    • @@ -100,7 +100,7 @@
      callback: nullOptional options: boolean | EventListenerOptions

    Returns void

    +
  • Defined in src/Evented.ts:14
  • @@ -114,7 +114,7 @@
    callback: nullOptional options: boolean | EventListenerOptions
  • Returns void

    +
  • Defined in src/Evented.ts:18
  • diff --git a/package-lock.json b/package-lock.json index e45c68e..62665db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,9 @@ "tsconfig-paths": "^3.9.0", "typedoc": "^0.23.21", "typescript": "^4.9.3" + }, + "engines": { + "node": ">=19.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 70c5782..e124bf4 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,8 @@ "tsconfig-paths": "^3.9.0", "typedoc": "^0.23.21", "typescript": "^4.9.3" + }, + "engines": { + "node": ">=19.0.0" } } diff --git a/pkgs.nix b/pkgs.nix index bb50140..2997ae2 100644 --- a/pkgs.nix +++ b/pkgs.nix @@ -1,4 +1,4 @@ import ( - let rev = "f294325aed382b66c7a188482101b0f336d1d7db"; in + let rev = "ea5234e7073d5f44728c499192544a84244bf35a"; in builtins.fetchTarball "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz" ) diff --git a/shell.nix b/shell.nix index a663138..54ec565 100644 --- a/shell.nix +++ b/shell.nix @@ -3,7 +3,7 @@ with pkgs; mkShell { nativeBuildInputs = [ - nodejs + nodejs_20 shellcheck gitAndTools.gh ]; diff --git a/src/AbstractEvent.ts b/src/AbstractEvent.ts new file mode 100644 index 0000000..1db160b --- /dev/null +++ b/src/AbstractEvent.ts @@ -0,0 +1,52 @@ +/** + * By default the `detail` property of `CustomEvent` is `null`. + * So the default type has to be `null` too. + */ +class AbstractEvent extends CustomEvent { + protected constructorParams: IArguments; + + public constructor( + type?: string, + options?: CustomEventInit, + constructorParams?: IArguments, + ); + public constructor( + options: CustomEventInit, + constructorParams?: IArguments, + ); + public constructor( + type: string | CustomEventInit = new.target.name, + options?: CustomEventInit | IArguments, + constructorParams?: IArguments, + ) { + if (typeof type === 'string') { + super(type, options as CustomEventInit | undefined); + } else { + super(new.target.name, type); + constructorParams = options as IArguments | undefined; + } + this.constructorParams = constructorParams ?? arguments; + } + + /** + * Events cannot be re-dispatched. This was probably to prevent infinite + * loops. So instead of re-dispatching the same instance, we re-dispatch + * a clone of the instance. + */ + public clone(): this { + try { + return new (this.constructor as any)(...this.constructorParams); + } catch (e) { + // Normally we would check `instanceof TypeError` + // However jest appears to choke on that + if (e.name === 'TypeError') { + throw new TypeError( + `Cloning ${this.constructor.name} requires the original constructor arguments to be passed into super`, + ); + } + throw e; + } + } +} + +export default AbstractEvent; diff --git a/src/EventAny.ts b/src/EventAny.ts index fc47704..6e262e7 100644 --- a/src/EventAny.ts +++ b/src/EventAny.ts @@ -1,12 +1,11 @@ -// @ts-ignore package.json is outside rootDir +import AbstractEvent from './AbstractEvent'; +// @ts-ignore package.json is outside root dir import { name } from '../package.json'; -class EventAny extends Event { +class EventAny extends AbstractEvent { public static type = `${name}/${this.name}`; - public detail: Event; - constructor(options: EventInit & { detail: Event }) { - super(EventAny.type, options); - this.detail = options.detail; + public constructor(options: EventInit & { detail: T }) { + super(EventAny.type, options, arguments); } } diff --git a/src/Evented.ts b/src/Evented.ts index 93c0dae..e2ea033 100644 --- a/src/Evented.ts +++ b/src/Evented.ts @@ -11,7 +11,6 @@ interface Evented { callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean, ): void; - dispatchEvent(event: Event): boolean; removeEventListener( callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean, @@ -21,6 +20,7 @@ interface Evented { callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean, ): void; + dispatchEvent(event: Event): boolean; } function Evented() { diff --git a/src/index.ts b/src/index.ts index 901332d..bd9c4d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ -export { Evented } from './Evented'; +export { default as AbstractEvent } from './AbstractEvent'; export { default as EventAny } from './EventAny'; +export { Evented } from './Evented'; export * as utils from './utils'; diff --git a/tests/index.test.ts b/tests/index.test.ts index e1d2c4e..4cd45c8 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,6 +1,47 @@ -import { Evented, EventAny, utils } from '@'; +import { AbstractEvent, EventAny, Evented, utils } from '@'; -describe('Evented', () => { +describe('index', () => { + test('abstract event subclasses', () => { + class Event0 extends AbstractEvent {} + class Event1 extends AbstractEvent {} + class Event2 extends AbstractEvent {} + class Event3 extends AbstractEvent {} + const e0 = new Event0(); + expect(e0.detail).toBeNull(); + const e1 = new Event1(); + expect(e1.detail).toBeNull(); + const e1_ = new Event1({ detail: 'string' }); + expect(e1_.detail).toBe('string'); + // This is not caught by the type system + const e2 = new Event2(); + expect(e2.detail).toBeNull(); + const e2_ = new Event2({ detail: 'string' }); + expect(e2_.detail).toBe('string'); + const e11 = e1.clone(); + expect(e11.type).toBe(e1.type); + expect(e11.detail).toBe(e1.detail); + const e22 = e2.clone(); + expect(e22.type).toBe(e2.type); + expect(e22.detail).toBe(e2.detail); + const e3 = new Event3({ detail: { a: 1 } }); + expect(e3.detail).toBe(e3.clone().detail); + }); + test('abstract event subclasses with different constructors', () => { + class EventCustom1 extends AbstractEvent { + constructor(options: CustomEventInit) { + super(EventCustom1.name, options); + } + } + class EventCustom2 extends AbstractEvent { + constructor(options: CustomEventInit) { + super(EventCustom2.name, options, arguments); + } + } + const e1 = new EventCustom1({ detail: 'string' }); + expect(() => e1.clone()).toThrow(TypeError); + const e2 = new EventCustom2({ detail: 'string' }); + expect(() => e2.clone()).not.toThrowError(); + }); test('can access event target via symbol', () => { interface X extends Evented {} @Evented()