Skip to content

Commit

Permalink
feat(fromEvent): support NodeJS Event Emitters
Browse files Browse the repository at this point in the history
PR #73.

* Support EventEmitters in fromEvent extra

Add support for stream generation from EventEmitters.

fromEvent will accept as a first argument either a DOMElement or an
EventEmitter. When providing fromEvent with an EventEmitter, the
third argument is always ignored.

Internally, include an additional Producer type within fromEvent to
support type checking.

Related to #65.

* Revert indent change

Revert indentation change to remain consistent with project style.

* Refactor guards; Conform test and source style

Introduce style changes to enforce project source consistency. Adjust
type checking within the fromEvent function. Specifically, duck type the
first argument rather than rely on `instanceof` or `typeof` guards when
deciding which producer type to instantiate.

* Remove whitespace to align with project style requirements.
* Remove destructuring assignment in producer class methods.
* Update documentation to include both uses of the extra.
* Remove whitespaces and semicolons from example markdown.
* Refactor producer instantiation guards to implement duck-typing rather
  than `instanceof`/`typeof` checking.
* Remove unused import from tests
* Reorder assertion equality arguments.
* Rename parameter in fromEvent documentation (`eventType`>`eventName`)

* Expand emitter guard

Check for both `addListener` and `emit` methods on an element supplied
to fromEvent. This ensures that the function will not attempt to invoke
the `addListener` method on an xstream stream instance with incorrect
arguments. Instead, this scenario will throw a TypeError:

'this.node.addEventListener is not a function'
  • Loading branch information
Christian Johns authored and staltz committed Jul 11, 2016
1 parent c0071c6 commit c203801
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 23 deletions.
76 changes: 61 additions & 15 deletions src/extra/fromEvent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/// <reference path="../../typings/globals/node/index.d.ts" />
import {EventEmitter} from 'events';
import {Stream, InternalProducer, InternalListener} from '../core';

export class DOMEventProducer implements InternalProducer<Event> {
Expand All @@ -11,29 +13,49 @@ export class DOMEventProducer implements InternalProducer<Event> {

_start(out: InternalListener<Event>) {
this.listener = (e) => out._n(e);
const {node, eventType, useCapture} = this;
node.addEventListener(eventType, this.listener, useCapture);
this.node.addEventListener(this.eventType, this.listener, this.useCapture);
}

_stop() {
const {node, eventType, listener, useCapture} = this;
node.removeEventListener(eventType, listener, useCapture);
this.node.removeEventListener(this.eventType, this.listener, this.useCapture);
this.listener = null;
}
}

export class NodeEventProducer implements InternalProducer<any> {
public type = 'fromEvent';
private listener: Function;

constructor(private node: EventEmitter, private eventName: string) { }

_start(out: InternalListener<any>) {
this.listener = (e: any) => out._n(e);
this.node.addListener(this.eventName, this.listener);
}

_stop() {
this.node.removeListener(this.eventName, this.listener);
this.listener = null;
}
}

function isEmitter(element: any): boolean {
return element.emit && element.addListener;
}

/**
* Creates a stream based on DOM events of type `eventType` from the target
* node.
* Creates a stream based on either:
* - DOM events with the name `eventName` from a provided target node
* - Events with the name `eventName` from a provided NodeJS EventEmitter
*
* Marble diagram:
*
* ```text
* fromEvent(node, eventType)
* fromEvent(element, eventName)
* ---ev--ev----ev---------------
* ```
*
* Example:
* Examples:
*
* ```js
* import fromEvent from 'xstream/extra/fromEvent'
Expand All @@ -54,15 +76,39 @@ export class DOMEventProducer implements InternalProducer<Event> {
* > 'Button clicked!'
* ```
*
* @param {EventTarget} node The element we want to listen to.
* @param {string} eventType The type of events we want to listen to.
* @param {boolean} useCapture An optional boolean that indicates that events of
* ```js
* import fromEvent from 'xstream/extra/fromEvent'
* import {EventEmitter} from 'events'
*
* const MyEmitter = new EventEmitter()
* const stream = fromEvent(MyEmitter, 'foo')
*
* stream.addListener({
* next: i => console.log(i),
* error: err => console.error(err),
* complete: () => console.log('completed')
* })
*
* MyEmitter.emit('foo', 'bar')
* ```
*
* ```text
* > 'bar'
* ```
*
* @param {EventTarget|EventEmitter} element The element upon which to listen.
* @param {string} eventName The name of the event for which to listen.
* @param {boolean?} useCapture An optional boolean that indicates that events of
* this type will be dispatched to the registered listener before being
* dispatched to any EventTarget beneath it in the DOM tree. Defaults to false.
* @return {Stream}
*/
export default function fromEvent(node: EventTarget,
eventType: string,
useCapture: boolean = false): Stream<Event> {
return new Stream<Event>(new DOMEventProducer(node, eventType, useCapture));
export default function fromEvent(element: EventTarget | EventEmitter,
eventName: string,
useCapture: boolean = false): Stream<Event|any> {
if (isEmitter(element)) {
return new Stream<any>(new NodeEventProducer(<EventEmitter> element, eventName));
} else {
return new Stream<Event>(new DOMEventProducer(<EventTarget> element, eventName, useCapture));
}
}
101 changes: 93 additions & 8 deletions tests/extra/fromEvent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference path="../../typings/globals/mocha/index.d.ts" />
/// <reference path="../../typings/globals/node/index.d.ts" />
import xs from '../../src/index';
import {EventEmitter} from 'events';
import fromEvent from '../../src/extra/fromEvent';
import * as assert from 'assert';
function noop() {};
Expand Down Expand Up @@ -39,15 +39,47 @@ class FakeEventTarget implements EventTarget {
}
};

describe('fromEvent (extra)', () => {
class FakeEventEmitter extends EventEmitter {
public handler: Function;
public event: string;
public removedEvent: string;

constructor() {
super();
}

emit( eventName: string, ...args: any[] ): any {
if (typeof this.handler !== 'function') {
return;
}
this.handler.apply(void 0, args );
return true;
}

addListener(e: string, handler: Function): FakeEventEmitter {
this.event = e;
this.handler = handler;
return this;
}

removeListener(e: string, handler: Function): FakeEventEmitter {
this.removedEvent = e;

this.handler = this.event = void 0;
return this;
}

};

describe('fromEvent (extra) - DOMEvent', () => {
it('should call addEventListener with expected parameters', () => {
const target = new FakeEventTarget();
const stream = fromEvent(target, 'test', true);

stream.addListener({next: noop, error: noop, complete: noop});

assert.strictEqual('test', target.event);
assert.strictEqual(true, target.capture);
assert.strictEqual(target.event, 'test');
assert.strictEqual(target.capture, true);
});

it('should call addEventListener with expected parameters', () => {
Expand All @@ -56,8 +88,8 @@ describe('fromEvent (extra)', () => {

stream.addListener({next: noop, error: noop, complete: noop});

assert.strictEqual('test', target.event);
assert.strictEqual(false, target.capture);
assert.strictEqual(target.event, 'test');
assert.strictEqual(target.capture, false);
});

it('should propagate events', (done) => {
Expand Down Expand Up @@ -92,8 +124,8 @@ describe('fromEvent (extra)', () => {
error: (err: any) => done(err),
complete() {
setTimeout(() => {
assert.strictEqual('test', target.removedEvent);
assert.strictEqual(true, target.removedCapture);
assert.strictEqual(target.removedEvent, 'test');
assert.strictEqual(target.removedCapture, true);
done();
}, 5);
}
Expand All @@ -103,3 +135,56 @@ describe('fromEvent (extra)', () => {
target.emit(2);
});
});

describe('fromEvent (extra) - EventEmitter', () => {
it('should call addListener with expected parameters', () => {
const target = new FakeEventEmitter();
const stream = fromEvent(target, 'test');

stream.addListener({next: noop, error: noop, complete: noop});

assert.strictEqual(target.event, 'test');
});

it('should propagate events', (done) => {
const target = new FakeEventEmitter();
const stream = fromEvent(target, 'test').take(3);

let expected = [1, 2, 3];

stream.addListener({
next: (x: any) => {
assert.strictEqual(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.strictEqual(expected.length, 0);
done();
}
});

target.emit( 'test', 1 );
target.emit( 'test', 2 );
target.emit( 'test', 3 );
target.emit( 'test', 4 );
});

it('should call removeListener with expected parameters', (done) => {
const target = new FakeEventEmitter();
const stream = fromEvent(target, 'test');

stream.take(1).addListener({
next: (x) => {},
error: (err: any) => done(err),
complete() {
setTimeout(() => {
assert.strictEqual(target.removedEvent, 'test');
done();
}, 5);
}
});

target.emit( 'test', 1 );
target.emit( 'test', 2 );
});
});

0 comments on commit c203801

Please sign in to comment.