From 1969f18c5f6d01cb60c9b57fc2d60fa6374dc492 Mon Sep 17 00:00:00 2001 From: Andrew Crites Date: Mon, 11 Jun 2018 13:57:27 -0400 Subject: [PATCH] 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. --- spec/observables/fromEvent-spec.ts | 79 +++++++++++++++++++++++++++- src/internal/observable/fromEvent.ts | 10 +++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/spec/observables/fromEvent-spec.ts b/spec/observables/fromEvent-spec.ts index 673e400b7d..fcb3251b5e 100644 --- a/spec/observables/fromEvent-spec.ts +++ b/spec/observables/fromEvent-spec.ts @@ -1,9 +1,12 @@ import { expect } from 'chai'; import { expectObservable } from '../helpers/marble-testing'; -import { fromEvent, NEVER, timer, pipe } from 'rxjs'; +import { Observable, fromEvent, NEVER, timer, pipe } from 'rxjs'; +import { NodeStyleEventEmitter, NodeCompatibleEventEmitter, NodeEventHandler } from 'rxjs/internal/observable/fromEvent'; import { mapTo, take, concat } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; +declare const type: Function; + declare function asDiagram(arg: string): Function; declare const rxTestScheduler: TestScheduler; @@ -89,7 +92,7 @@ describe('fromEvent', () => { expect(offHandler).to.equal(onHandler); }); - it('should setup an event observable on objects with "addListener" and "removeListener" ', () => { + it('should setup an event observable on objects with "addListener" and "removeListener" returning event emitter', () => { let onEventName; let onHandler; let offEventName; @@ -121,6 +124,37 @@ describe('fromEvent', () => { expect(offHandler).to.equal(onHandler); }); + it('should setup an event observable on objects with "addListener" and "removeListener" returning nothing', () => { + let onEventName; + let onHandler; + let offEventName; + let offHandler; + + const obj = { + addListener(a: string, b: (...args: any[]) => any, context?: any): { context: any } { + onEventName = a; + onHandler = b; + return { context: '' }; + }, + removeListener(a: string, b: (...args: any[]) => void) { + offEventName = a; + offHandler = b; + } + }; + + const subscription = fromEvent(obj, 'click') + .subscribe(() => { + //noop + }); + + subscription.unsubscribe(); + + expect(onEventName).to.equal('click'); + expect(typeof onHandler).to.equal('function'); + expect(offEventName).to.equal(onEventName); + expect(offHandler).to.equal(onHandler); + }); + it('should setup an event observable on objects with "addListener" and "removeListener" and "length" ', () => { let onEventName; let onHandler; @@ -363,4 +397,45 @@ describe('fromEvent', () => { }).to.not.throw(TypeError); }); + type('should support node style event emitters interfaces', () => { + /* tslint:disable:no-unused-variable */ + let a: NodeStyleEventEmitter; + let b: Observable = fromEvent(a, 'mock'); + /* tslint:enable:no-unused-variable */ + }); + + type('should support node compatible event emitters interfaces', () => { + /* tslint:disable:no-unused-variable */ + let a: NodeCompatibleEventEmitter; + let b: Observable = fromEvent(a, 'mock'); + /* tslint:enable:no-unused-variable */ + }); + + type('should support node style event emitters objects', () => { + /* tslint:disable:no-unused-variable */ + interface NodeEventEmitter { + addListener(eventType: string | symbol, handler: NodeEventHandler): this; + removeListener(eventType: string | symbol, handler: NodeEventHandler): this; + } + let a: NodeEventEmitter; + let b: Observable = fromEvent(a, 'mock'); + /* tslint:enable:no-unused-variable */ + }); + + type('should support React Native event emitters', () => { + /* tslint:disable:no-unused-variable */ + interface EmitterSubscription { + context: any; + } + interface ReactNativeEventEmitterListener { + addListener(eventType: string, listener: (...args: any[]) => any, context?: any): EmitterSubscription; + } + interface ReactNativeEventEmitter extends ReactNativeEventEmitterListener { + removeListener(eventType: string, listener: (...args: any[]) => any): void; + } + let a: ReactNativeEventEmitter; + let b: Observable = fromEvent(a, 'mock'); + /* tslint:enable:no-unused-variable */ + }); + }); diff --git a/src/internal/observable/fromEvent.ts b/src/internal/observable/fromEvent.ts index e6019b0152..c8b8c6d4f0 100644 --- a/src/internal/observable/fromEvent.ts +++ b/src/internal/observable/fromEvent.ts @@ -13,6 +13,14 @@ export interface NodeStyleEventEmitter { export type NodeEventHandler = (...args: any[]) => void; +// For APIs that implement `addListener` and `removeListener` methods that may +// not use the same arguments or return EventEmitter values +// such as React Native +export interface NodeCompatibleEventEmitter { + addListener: (eventName: string, handler: NodeEventHandler) => void | {}; + removeListener: (eventName: string, handler: NodeEventHandler) => void | {}; +} + export interface JQueryStyleEventEmitter { on: (eventName: string, handler: Function) => void; off: (eventName: string, handler: Function) => void; @@ -23,7 +31,7 @@ export interface HasEventTargetAddRemove { removeEventListener(type: string, listener?: ((evt: E) => void) | null, options?: EventListenerOptions | boolean): void; } -export type EventTargetLike = HasEventTargetAddRemove | NodeStyleEventEmitter | JQueryStyleEventEmitter; +export type EventTargetLike = HasEventTargetAddRemove | NodeStyleEventEmitter | NodeCompatibleEventEmitter | JQueryStyleEventEmitter; export type FromEventTarget = EventTargetLike | ArrayLike>;