events: add `CustomEvent`

This implements the Web API `CustomEvent` in `internal/event_target`.

Signed-off-by: Daeyeon Jeong daeyeon.dev@gmail.com

PR-URL: https://github.com/nodejs/node/pull/43514
Refs: https://github.com/nodejs/node/issues/40678
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
Daeyeon Jeong 2022-07-17 20:27:49 +09:00 committed by GitHub
parent 4eb8e3fb58
commit cbbd91d804
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 428 additions and 3 deletions

View File

@ -1987,6 +1987,31 @@ added: v14.5.0
Removes the `listener` from the list of handlers for event `type`.
### Class: `CustomEvent`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental.
* Extends: {Event}
The `CustomEvent` object is an adaptation of the [`CustomEvent` Web API][].
Instances are created internally by Node.js.
#### `event.detail`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental.
* Type: {any} Returns custom data passed when initializing.
Read-only.
### Class: `NodeEventTarget`
<!-- YAML
@ -2124,6 +2149,7 @@ to the `EventTarget`.
[WHATWG-EventTarget]: https://dom.spec.whatwg.org/#interface-eventtarget
[`--trace-warnings`]: cli.md#--trace-warnings
[`CustomEvent` Web API]: https://dom.spec.whatwg.org/#customevent
[`EventTarget` Web API]: https://dom.spec.whatwg.org/#eventtarget
[`EventTarget` error handling]: #eventtarget-error-handling
[`Event` Web API]: https://dom.spec.whatwg.org/#event

View File

@ -67,6 +67,7 @@ const kTrustEvent = Symbol('kTrustEvent');
const { now } = require('internal/perf/utils');
const kType = Symbol('type');
const kDetail = Symbol('detail');
const isTrustedSet = new SafeWeakSet();
const isTrusted = ObjectGetOwnPropertyDescriptor({
@ -322,6 +323,49 @@ ObjectDefineProperties(
stopPropagation: kEnumerableProperty,
});
function isCustomEvent(value) {
return isEvent(value) && (value?.[kDetail] !== undefined);
}
class CustomEvent extends Event {
/**
* @constructor
* @param {string} type
* @param {{
* bubbles?: boolean,
* cancelable?: boolean,
* composed?: boolean,
* detail?: any,
* }} [options]
*/
constructor(type, options = kEmptyObject) {
if (arguments.length === 0)
throw new ERR_MISSING_ARGS('type');
super(type, options);
this[kDetail] = options?.detail ?? null;
}
/**
* @type {any}
*/
get detail() {
if (!isCustomEvent(this))
throw new ERR_INVALID_THIS('CustomEvent');
return this[kDetail];
}
}
ObjectDefineProperties(CustomEvent.prototype, {
[SymbolToStringTag]: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 'CustomEvent',
},
detail: kEnumerableProperty,
});
class NodeCustomEvent extends Event {
constructor(type, options) {
super(type, options);
@ -984,6 +1028,7 @@ const EventEmitterMixin = (Superclass) => {
module.exports = {
Event,
CustomEvent,
EventEmitterMixin,
EventTarget,
NodeEventTarget,

View File

@ -0,0 +1,325 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');
const { ok, strictEqual, deepStrictEqual, throws } = require('node:assert');
const { inspect } = require('node:util');
const { Event, EventTarget, CustomEvent } = require('internal/event_target');
{
ok(CustomEvent);
// Default string
const tag = Object.prototype.toString.call(new CustomEvent('$'));
strictEqual(tag, '[object CustomEvent]');
}
{
// No argument behavior - throw TypeError
throws(() => {
new CustomEvent();
}, TypeError);
throws(() => new CustomEvent(Symbol()), TypeError);
// Too many arguments passed behavior - ignore additional arguments
const ev = new CustomEvent('foo', {}, {});
strictEqual(ev.type, 'foo');
}
{
const ev = new CustomEvent('$');
strictEqual(ev.type, '$');
strictEqual(ev.bubbles, false);
strictEqual(ev.cancelable, false);
strictEqual(ev.detail, null);
}
{
// Coercion to string works
strictEqual(new CustomEvent(1).type, '1');
strictEqual(new CustomEvent(false).type, 'false');
strictEqual(new CustomEvent({}).type, String({}));
}
{
const ev = new CustomEvent('$', {
detail: 56,
sweet: 'x',
cancelable: true,
});
strictEqual(ev.type, '$');
strictEqual(ev.bubbles, false);
strictEqual(ev.cancelable, true);
strictEqual(ev.sweet, undefined);
strictEqual(ev.detail, 56);
}
{
// Any types of value for `detail` are acceptable.
['foo', 1, false, [], {}].forEach((i) => {
const ev = new CustomEvent('$', { detail: i });
strictEqual(ev.detail, i);
});
}
{
// Readonly `detail` behavior
const ev = new CustomEvent('$', {
detail: 56,
});
strictEqual(ev.detail, 56);
try {
ev.detail = 96;
// eslint-disable-next-line no-unused-vars
} catch (error) {
common.mustCall()();
}
strictEqual(ev.detail, 56);
}
{
const ev = new Event('$', {
detail: 96,
});
strictEqual(ev.detail, undefined);
}
// The following tests verify whether CustomEvent works the same as Event
// except carrying custom data. They're based on `parallel/test-eventtarget.js`.
{
const ev = new CustomEvent('$');
strictEqual(ev.type, '$');
strictEqual(ev.bubbles, false);
strictEqual(ev.cancelable, false);
strictEqual(ev.detail, null);
strictEqual(ev.defaultPrevented, false);
strictEqual(typeof ev.timeStamp, 'number');
// Compatibility properties with the DOM
deepStrictEqual(ev.composedPath(), []);
strictEqual(ev.returnValue, true);
strictEqual(ev.composed, false);
strictEqual(ev.isTrusted, false);
strictEqual(ev.eventPhase, 0);
strictEqual(ev.cancelBubble, false);
// Not cancelable
ev.preventDefault();
strictEqual(ev.defaultPrevented, false);
}
{
// Invalid options
['foo', 1, false].forEach((i) =>
throws(() => new CustomEvent('foo', i), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message:
'The "options" argument must be of type object.' +
common.invalidArgTypeHelper(i),
}),
);
}
{
const ev = new CustomEvent('$');
strictEqual(ev.constructor.name, 'CustomEvent');
// CustomEvent Statics
strictEqual(CustomEvent.NONE, 0);
strictEqual(CustomEvent.CAPTURING_PHASE, 1);
strictEqual(CustomEvent.AT_TARGET, 2);
strictEqual(CustomEvent.BUBBLING_PHASE, 3);
strictEqual(new CustomEvent('foo').eventPhase, CustomEvent.NONE);
// CustomEvent is a function
strictEqual(CustomEvent.length, 1);
}
{
const ev = new CustomEvent('foo');
strictEqual(ev.cancelBubble, false);
ev.cancelBubble = true;
strictEqual(ev.cancelBubble, true);
}
{
const ev = new CustomEvent('foo');
strictEqual(ev.cancelBubble, false);
ev.stopPropagation();
strictEqual(ev.cancelBubble, true);
}
{
const ev = new CustomEvent('foo');
strictEqual(ev.cancelBubble, false);
ev.cancelBubble = 'some-truthy-value';
strictEqual(ev.cancelBubble, true);
}
{
const ev = new CustomEvent('foo');
strictEqual(ev.cancelBubble, false);
ev.cancelBubble = true;
strictEqual(ev.cancelBubble, true);
}
{
const ev = new CustomEvent('foo');
strictEqual(ev.cancelBubble, false);
ev.stopPropagation();
strictEqual(ev.cancelBubble, true);
}
{
const ev = new CustomEvent('foo');
strictEqual(ev.cancelBubble, false);
ev.cancelBubble = 'some-truthy-value';
strictEqual(ev.cancelBubble, true);
}
{
const ev = new CustomEvent('foo', { cancelable: true });
strictEqual(ev.type, 'foo');
strictEqual(ev.cancelable, true);
strictEqual(ev.defaultPrevented, false);
ev.preventDefault();
strictEqual(ev.defaultPrevented, true);
}
{
const ev = new CustomEvent('foo');
deepStrictEqual(Object.keys(ev), ['isTrusted']);
}
// Works with EventTarget
{
const obj = { sweet: 'x', memory: { x: 56, y: 96 } };
const et = new EventTarget();
const ev = new CustomEvent('$', { detail: obj });
const fn = common.mustCall((event) => {
strictEqual(event, ev);
deepStrictEqual(event.detail, obj);
});
et.addEventListener('$', fn);
et.dispatchEvent(ev);
}
{
const eventTarget = new EventTarget();
const event = new CustomEvent('$');
eventTarget.dispatchEvent(event);
strictEqual(event.target, eventTarget);
}
{
const obj = { sweet: 'x' };
const eventTarget = new EventTarget();
const ev1 = common.mustCall(function(event) {
strictEqual(event.type, 'foo');
strictEqual(event.detail, obj);
strictEqual(this, eventTarget);
strictEqual(event.eventPhase, 2);
}, 2);
const ev2 = {
handleEvent: common.mustCall(function(event) {
strictEqual(event.type, 'foo');
strictEqual(event.detail, obj);
strictEqual(this, ev2);
}),
};
eventTarget.addEventListener('foo', ev1);
eventTarget.addEventListener('foo', ev2, { once: true });
ok(eventTarget.dispatchEvent(new CustomEvent('foo', { detail: obj })));
eventTarget.dispatchEvent(new CustomEvent('foo', { detail: obj }));
eventTarget.removeEventListener('foo', ev1);
eventTarget.dispatchEvent(new CustomEvent('foo'));
}
{
// Same event dispatched multiple times.
const obj = { sweet: 'x' };
const event = new CustomEvent('foo', { detail: obj });
const eventTarget1 = new EventTarget();
const eventTarget2 = new EventTarget();
eventTarget1.addEventListener(
'foo',
common.mustCall((event) => {
strictEqual(event.eventPhase, CustomEvent.AT_TARGET);
strictEqual(event.target, eventTarget1);
strictEqual(event.detail, obj);
deepStrictEqual(event.composedPath(), [eventTarget1]);
}),
);
eventTarget2.addEventListener(
'foo',
common.mustCall((event) => {
strictEqual(event.eventPhase, CustomEvent.AT_TARGET);
strictEqual(event.target, eventTarget2);
strictEqual(event.detail, obj);
deepStrictEqual(event.composedPath(), [eventTarget2]);
}),
);
eventTarget1.dispatchEvent(event);
strictEqual(event.eventPhase, CustomEvent.NONE);
strictEqual(event.target, eventTarget1);
deepStrictEqual(event.composedPath(), []);
eventTarget2.dispatchEvent(event);
strictEqual(event.eventPhase, CustomEvent.NONE);
strictEqual(event.target, eventTarget2);
deepStrictEqual(event.composedPath(), []);
}
{
const obj = { sweet: 'x' };
const target = new EventTarget();
const event = new CustomEvent('foo', { detail: obj });
strictEqual(event.target, null);
target.addEventListener(
'foo',
common.mustCall((event) => {
strictEqual(event.target, target);
strictEqual(event.currentTarget, target);
strictEqual(event.srcElement, target);
strictEqual(event.detail, obj);
}),
);
target.dispatchEvent(event);
}
{
// Event subclassing
const SubEvent = class extends CustomEvent {};
const ev = new SubEvent('foo', { detail: 56 });
const eventTarget = new EventTarget();
const fn = common.mustCall((event) => {
strictEqual(event, ev);
strictEqual(event.detail, 56);
});
eventTarget.addEventListener('foo', fn, { once: true });
eventTarget.dispatchEvent(ev);
}
// Works with inspect
{
const ev = new CustomEvent('test');
const evConstructorName = inspect(ev, {
depth: -1,
});
strictEqual(evConstructorName, 'CustomEvent');
const inspectResult = inspect(ev, {
depth: 1,
});
ok(inspectResult.includes('CustomEvent'));
}

View File

@ -6,6 +6,7 @@ const assert = require('assert');
const {
Event,
CustomEvent,
EventTarget,
NodeEventTarget,
} = require('internal/event_target');
@ -41,10 +42,37 @@ const {
});
[
'addEventListener',
'removeEventListener',
'dispatchEvent',
'target',
'currentTarget',
'srcElement',
'type',
'cancelable',
'defaultPrevented',
'timeStamp',
'returnValue',
'bubbles',
'composed',
'eventPhase',
'detail',
].forEach((i) => {
assert.throws(() => Reflect.get(CustomEvent.prototype, i, {}), {
code: 'ERR_INVALID_THIS',
});
});
[
'stopImmediatePropagation',
'preventDefault',
'composedPath',
'cancelBubble',
'stopPropagation',
].forEach((i) => {
assert.throws(() => Reflect.apply(CustomEvent.prototype[i], [], {}), {
code: 'ERR_INVALID_THIS',
});
});
['addEventListener', 'removeEventListener', 'dispatchEvent'].forEach((i) => {
assert.throws(() => Reflect.apply(EventTarget.prototype[i], [], {}), {
code: 'ERR_INVALID_THIS',
});

View File

@ -117,6 +117,7 @@ const customTypesMap = {
'EventEmitter': 'events.html#class-eventemitter',
'EventTarget': 'events.html#class-eventtarget',
'Event': 'events.html#class-event',
'CustomEvent': 'events.html#class-customevent',
'EventListener': 'events.html#event-listener',
'FileHandle': 'fs.html#class-filehandle',