Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
- Basic implementation
- Basic tests as a simple script
- Update README
  • Loading branch information
benlesh committed Jul 14, 2020
1 parent 17ba8ed commit 3902db6
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 1 deletion.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
# event-target-polyfill
An EventTarget Polyfill

A polyfill for `EventTarget` (and `Event`), meant to run in older version of node or possibly IE 11, that has the most accurate set of characteristics of `EventTarget` that can be provided.

If you find this implementation can be improved, please submit a PR and ping me [on Twitter via DM](https://twitter.com/benlesh).

**NOTE: If you are using Node 14 and higher, [EventTarget is available directly](https://nodejs.org/api/events.html#events_eventtarget_and_event_api) via experimental features**
MDN: [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)

## Usage

```
import '@benlesh/event-target-polyfill';
const et = new EventTarget();
et.addEventListener('test', () => console.log('hit!'));
et.dispatchEvent(new Event('test'));
```

## Development

This library has no dependencies. Even development dependencies. To test just run `npm test`. It runs a script, and if it finishes without error, the tests pass.

116 changes: 116 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const root =
(typeof globalThis !== "undefined" && globalThis) ||
(typeof self !== "undefined" && self) ||
(typeof global !== "undefined" && global);

if (typeof root.Event === "undefined") {
root.Event = (function () {
function Event(type, options) {
if (options) {
for (let key of options) {
if (options.hasOwnProperty(key)) {
this[key] = options[key];
}
}
}
this.type = type;
}

return Event;
})();
}

if (typeof root.EventTarget === "undefined") {
root.EventTarget = (function () {
function EventTarget() {
this.__listeners = new Map();
}

EventTarget.prototype = Object.create(Object.prototype);

EventTarget.prototype.addEventListener = function (
type,
listener,
options
) {
if (arguments.length < 2) {
throw new TypeError(
`TypeError: Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only ${arguments.length} present.`
);
}
const __listeners = this.__listeners;
const actualType = type.toString();
if (!__listeners.has(actualType)) {
__listeners.set(actualType, new Map());
}
const listenersForType = __listeners.get(actualType);
if (!listenersForType.has(listener)) {
// Any given listener is only registered once
listenersForType.set(listener, options);
}
};

EventTarget.prototype.removeEventListener = function (
type,
listener,
_options
) {
if (arguments.length < 2) {
throw new TypeError(
`TypeError: Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only ${arguments.length} present.`
);
}
const __listeners = this.__listeners;
const actualType = type.toString();
if (__listeners.has(actualType)) {
const listenersForType = __listeners.get(actualType);
if (listenersForType.has(listener)) {
listenersForType.delete(listener);
}
}
};

EventTarget.prototype.dispatchEvent = function (event) {
if (!(event instanceof Event)) {
throw new TypeError(
`Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.`
);
}
const type = event.type;
const __listeners = this.__listeners;
const listenersForType = __listeners.get(type);
if (listenersForType) {
for (const [listener, options] of listenersForType.entries()) {
try {
if (typeof listener === "function") {
// Listener functions must be executed with the EventTarget as the `this` context.
listener.call(this, event);
} else if (listener && typeof listener.handleEvent === "function") {
// Listener objects have their handleEvent method called, if they have one
listener.handleEvent(event);
}
} catch (err) {
// We need to report the error to the global error handling event,
// but we do not want to break the loop that is executing the events.
// Unfortunately, this is the best we can do, which isn't great, because the
// native EventTarget will actually do this synchronously before moving to the next
// event in the loop.
setTimeout(() => {
throw err;
});
}
if (options && options.once) {
// If this was registered with { once: true }, we need
// to remove it now.
listenersForType.delete(listener);
}
}
}
// Since there are no cancellable events on a base EventTarget,
// this should always return true.
return true;
};

return EventTarget;
})();
}
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@benlesh/event-target-polyfill",
"version": "0.0.1",
"description": "An EventTarget Polyfill",
"type": "module",
"main": "index.js",
"exports": {
".": "./index.js"
},
"dependencies": {},
"devDependencies": {},
"scripts": {
"test": "node ./test.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/benlesh/event-target-polyfill.git"
},
"keywords": [
"EventTarget",
"Polyfill"
],
"author": "Ben Lesh <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/benlesh/event-target-polyfill/issues"
},
"homepage": "https://github.com/benlesh/event-target-polyfill#readme"
}
206 changes: 206 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import "./index.js";

if (typeof EventTarget === "undefined") {
fail("EventTarget does not exist");
}

if (typeof Event === "undefined") {
fail("Event does not exist");
}

{
// Should pass the proper stuff to the listener function
const et = new EventTarget();

const event = new Event("test1");

et.addEventListener("test1", function (e) {
if (e.type !== "test1") {
fail(`Incorrect event type ${e.type}`);
}
if (e !== event) {
fail("Event instance not passed to listener");
}
if (this !== et) {
fail(`context should be the EventTarget that dispatched`);
}
});

et.dispatchEvent(event);
}

{
// adding and removing event listeners should work

let handlerCalls = 0;
const handler = () => handlerCalls++;
const listener = {
handleEvent() {
this.calls++;
},
calls: 0
};

const et = new EventTarget();

et.addEventListener('registration', handler);
et.addEventListener('registration', listener);

et.dispatchEvent(new Event('registration'));
et.dispatchEvent(new Event('registration'));

if (listener.calls !== 2) {
fail(`Expected 2 calls to listener.handleEvent, got ${listener.calls}.`);
}
if (handlerCalls !== 2) {
fail(`Expected 2 calls to handler, got ${handlerCalls}.`);
}

et.removeEventListener('registration', handler);

et.dispatchEvent(new Event('registration'));
et.dispatchEvent(new Event('registration'));

if (listener.calls !== 4) {
fail(`Expected 4 calls to listener.handleEvent, got ${listener.calls}.`);
}
if (handlerCalls !== 2) {
fail(`Expected 2 calls to handler, got ${handlerCalls}.`);
}

et.removeEventListener('registration', listener);
et.dispatchEvent(new Event('registration'));
et.dispatchEvent(new Event('registration'));

if (listener.calls !== 4) {
fail(`Expected 4 calls to listener.handleEvent, got ${listener.calls}.`);
}
if (handlerCalls !== 2) {
fail(`Expected 2 calls to handler, got ${handlerCalls}.`);
}
}

{
// Registering the same handler more than once should be idempotent
const et = new EventTarget();

let handlerCalls = 0;
const handler = () => {
handlerCalls++;
};

et.addEventListener('idem', handler);
et.addEventListener('idem', handler, { once: true });
et.addEventListener('idem', handler);
et.addEventListener('idem', handler);

et.dispatchEvent(new Event('idem'));

if (handlerCalls !== 1) {
fail(`Expected handler to have been called once. Was called ${handlerCalls} times`);
}
}


{
// Should handle registration of listeners that only fire once
// using the options argument
const et = new EventTarget();

let calls = 0;
et.addEventListener(
"testOnce",
function () {
calls++;
},
{ once: true }
);

et.dispatchEvent(new Event("testOnce"));
et.dispatchEvent(new Event("testOnce"));
et.dispatchEvent(new Event("testOnce"));

if (calls !== 1) {
fail(`Registering once did not work. Expected 1 call, got ${calls}.`);
}
}

{
// addEventListener Should not throw if boolean is passed as the third argument
const et = new EventTarget();

et.addEventListener('test', function () { }, true);
}

{
// Should handle registration of listener objects.
const et = new EventTarget();

const listener = {
handleEvent() {
if (this !== listener) {
fail("Expected context to be the listener object itself");
}
this.calls++;
},
calls: 0,
};

et.addEventListener("listenerObject", listener);

et.dispatchEvent(new Event("listenerObject"));
et.dispatchEvent(new Event("listenerObject"));
et.dispatchEvent(new Event("listenerObject"));

if (listener.calls !== 3) {
fail(
`handleEvent should have been called 3 times, called ${listener.calls} times.`
);
}
}

{
// dispatchEvent should return true
const et = new EventTarget();
const defaultNotPrevented = et.dispatchEvent(new Event("test"));

if (defaultNotPrevented !== true) {
fail(
"basic EventTarget does not have cancellable events, so dispatchEvent should always return true"
);
}
}

{
// Events should be dispatched synchronous
const et = new EventTarget();

const order = [];
let n = 0;
et.addEventListener("sync", () => {
order.push(n++, "event");
});

order.push(n++, "start");
et.dispatchEvent(new Event("sync"));
et.dispatchEvent(new Event("sync"));
et.dispatchEvent(new Event("sync"));
order.push(n++, "end");

if (
order.join(",") !==
[0, "start", 1, "event", 2, "event", 3, "event", 4, "end"].join(",")
) {
fail("Events not triggered synchronously");
}
}

// Kill the node process with a failure message if a test
// is failing.
function fail(reason) {
console.error(reason);
process.exit(1);
}

// We've reached the end of this test script
console.log('All tests pass');

0 comments on commit 3902db6

Please sign in to comment.