Skip to content

Commit 1969f18

Browse files
ajcritesbenlesh
authored andcommitted
fix(fromEvent): Support React Native and node-compatible event sources. (#3821)
React Native implements a similar interface to Node.js event listeners -- specifically, its Event Emitters have addListener and removeListener methods. These are compatible with the Node interface, but they are not identical to the Node.js *style* since the addListener method returns a subscription rather than the event emitter itself, and the removeListener method returns nothing. These return values are not actually used by fromEvent, so creating an Observable from an Event Emitter where addListener returns something different and removeListener returns nothing should still work to create a compatible Observable that emits with the corresponding events.
1 parent 542fb88 commit 1969f18

File tree

2 files changed

+86
-3
lines changed

2 files changed

+86
-3
lines changed

spec/observables/fromEvent-spec.ts

+77-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { expect } from 'chai';
22
import { expectObservable } from '../helpers/marble-testing';
3-
import { fromEvent, NEVER, timer, pipe } from 'rxjs';
3+
import { Observable, fromEvent, NEVER, timer, pipe } from 'rxjs';
4+
import { NodeStyleEventEmitter, NodeCompatibleEventEmitter, NodeEventHandler } from 'rxjs/internal/observable/fromEvent';
45
import { mapTo, take, concat } from 'rxjs/operators';
56
import { TestScheduler } from 'rxjs/testing';
67

8+
declare const type: Function;
9+
710
declare function asDiagram(arg: string): Function;
811
declare const rxTestScheduler: TestScheduler;
912

@@ -89,7 +92,7 @@ describe('fromEvent', () => {
8992
expect(offHandler).to.equal(onHandler);
9093
});
9194

92-
it('should setup an event observable on objects with "addListener" and "removeListener" ', () => {
95+
it('should setup an event observable on objects with "addListener" and "removeListener" returning event emitter', () => {
9396
let onEventName;
9497
let onHandler;
9598
let offEventName;
@@ -121,6 +124,37 @@ describe('fromEvent', () => {
121124
expect(offHandler).to.equal(onHandler);
122125
});
123126

127+
it('should setup an event observable on objects with "addListener" and "removeListener" returning nothing', () => {
128+
let onEventName;
129+
let onHandler;
130+
let offEventName;
131+
let offHandler;
132+
133+
const obj = {
134+
addListener(a: string, b: (...args: any[]) => any, context?: any): { context: any } {
135+
onEventName = a;
136+
onHandler = b;
137+
return { context: '' };
138+
},
139+
removeListener(a: string, b: (...args: any[]) => void) {
140+
offEventName = a;
141+
offHandler = b;
142+
}
143+
};
144+
145+
const subscription = fromEvent(obj, 'click')
146+
.subscribe(() => {
147+
//noop
148+
});
149+
150+
subscription.unsubscribe();
151+
152+
expect(onEventName).to.equal('click');
153+
expect(typeof onHandler).to.equal('function');
154+
expect(offEventName).to.equal(onEventName);
155+
expect(offHandler).to.equal(onHandler);
156+
});
157+
124158
it('should setup an event observable on objects with "addListener" and "removeListener" and "length" ', () => {
125159
let onEventName;
126160
let onHandler;
@@ -363,4 +397,45 @@ describe('fromEvent', () => {
363397
}).to.not.throw(TypeError);
364398
});
365399

400+
type('should support node style event emitters interfaces', () => {
401+
/* tslint:disable:no-unused-variable */
402+
let a: NodeStyleEventEmitter;
403+
let b: Observable<any> = fromEvent(a, 'mock');
404+
/* tslint:enable:no-unused-variable */
405+
});
406+
407+
type('should support node compatible event emitters interfaces', () => {
408+
/* tslint:disable:no-unused-variable */
409+
let a: NodeCompatibleEventEmitter;
410+
let b: Observable<any> = fromEvent(a, 'mock');
411+
/* tslint:enable:no-unused-variable */
412+
});
413+
414+
type('should support node style event emitters objects', () => {
415+
/* tslint:disable:no-unused-variable */
416+
interface NodeEventEmitter {
417+
addListener(eventType: string | symbol, handler: NodeEventHandler): this;
418+
removeListener(eventType: string | symbol, handler: NodeEventHandler): this;
419+
}
420+
let a: NodeEventEmitter;
421+
let b: Observable<any> = fromEvent(a, 'mock');
422+
/* tslint:enable:no-unused-variable */
423+
});
424+
425+
type('should support React Native event emitters', () => {
426+
/* tslint:disable:no-unused-variable */
427+
interface EmitterSubscription {
428+
context: any;
429+
}
430+
interface ReactNativeEventEmitterListener {
431+
addListener(eventType: string, listener: (...args: any[]) => any, context?: any): EmitterSubscription;
432+
}
433+
interface ReactNativeEventEmitter extends ReactNativeEventEmitterListener {
434+
removeListener(eventType: string, listener: (...args: any[]) => any): void;
435+
}
436+
let a: ReactNativeEventEmitter;
437+
let b: Observable<any> = fromEvent(a, 'mock');
438+
/* tslint:enable:no-unused-variable */
439+
});
440+
366441
});

src/internal/observable/fromEvent.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ export interface NodeStyleEventEmitter {
1313

1414
export type NodeEventHandler = (...args: any[]) => void;
1515

16+
// For APIs that implement `addListener` and `removeListener` methods that may
17+
// not use the same arguments or return EventEmitter values
18+
// such as React Native
19+
export interface NodeCompatibleEventEmitter {
20+
addListener: (eventName: string, handler: NodeEventHandler) => void | {};
21+
removeListener: (eventName: string, handler: NodeEventHandler) => void | {};
22+
}
23+
1624
export interface JQueryStyleEventEmitter {
1725
on: (eventName: string, handler: Function) => void;
1826
off: (eventName: string, handler: Function) => void;
@@ -23,7 +31,7 @@ export interface HasEventTargetAddRemove<E> {
2331
removeEventListener(type: string, listener?: ((evt: E) => void) | null, options?: EventListenerOptions | boolean): void;
2432
}
2533

26-
export type EventTargetLike<T> = HasEventTargetAddRemove<T> | NodeStyleEventEmitter | JQueryStyleEventEmitter;
34+
export type EventTargetLike<T> = HasEventTargetAddRemove<T> | NodeStyleEventEmitter | NodeCompatibleEventEmitter | JQueryStyleEventEmitter;
2735

2836
export type FromEventTarget<T> = EventTargetLike<T> | ArrayLike<EventTargetLike<T>>;
2937

0 commit comments

Comments
 (0)