RxJS Mastery – #11 fromEvent

The fromEvent creation operator creates an Observable for specific event types on a specific element. So you can for example say that you are interested in click events on the HTML document:

const clicks$ = fromEvent(document, 'click');

// or more exact:
const clickEventsOnDocument$: Observable<MouseEvent> = fromEvent<MouseEvent>(document, 'click');

As a subscriber to $clicks or clickEventsOnDocument$ you are receiving all click events that happen on the document.

RxJS fromEvent explained 🎓

The primary signature of the operator looks as follows:

fromEvent<T>(
    target: any, 
    eventName: string,
    options?: EventListenerOptions | ((...args: any[]) => T), 
    resultSelector?: (...args: any[]) => T
): Observable<T>

So, we can not only pass the target and the event name, but the following two arguments:

  • EventListenerOptions that are known from the event handlers themselves
  • resultSelector to act on the returned events

But this is not the only variant of fromEvent. The function is overloaded and also accepts other parameters. One group of overloads are based on the interface HasEventTargetAddRemove which has two methods addEventListener and removeEventListener:

fromEvent(
    target: HasEventTargetAddRemove<T> | ArrayLike<HasEventTargetAddRemove<T>>,
    eventName: string
): Observable<T>

Another group of overloads is used for Node style event emitters having addListener and removeListener functions:

fromEvent(
    target: NodeStyleEventEmitter | ArrayLike<NodeStyleEventEmitter>,
    eventName: string
): Observable<unknown>

NodeCompatibleEventEmitter work similar to NodeStyleEventEmitter.
Yet another variant of overloads even supports JQueryStyleEventEmitter that internally are based on the on and off functions:

fromEvent(
    target: JQueryStyleEventEmitter<any, T> | ArrayLike<JQueryStyleEventEmitter<any, T>>,
    eventName: string
): Observable<T>

So, the pattern is always the same. The fromEvent creation operator accepts a construct that allows to add and remove event listeners. Adding the listener will allow to deliver values while removing the listener is used to tear down when unsubscribing.

What problems does the RxJS fromEvent operator solve? 🚧

The fromEvent creation operator can be used to create a stream of DOM events. But this is not the only way to use it as we have seen above with the overloads. Furthermore, the function can be used to also create Observable in other areas.

Node.js EventEmitter

As seen above we could also pass an NodeJs style event emitter to the fromEvent function. This event emitter is an object with addListener and removeListener methods.

JQuery-style event target

Every developer that started with jQuery in the early JavaScript days probably remembers the event handling. jQuery usually offers an on() and an off() method. The RxJS fromEvent operator also allows here to create an Observable.

DOM NodeList and DOM HtmlCollection

These variants are helpful if you need to add events on multiple DOM nodes. So, if you pass an array of nodes to the fromEvent operator the event handler function will be installed in every of them. Unsubscribing from the created Observable will trigger the tear down for all the nodes.

How to test the fromEvent operator🚦

You can wrap the fromEvent function with another one and apply some mocking. Alternatively you can mock the object passed to the function and trigger events like that.

it('should test by mocking the target', () => {
    testScheduler.run((helpers) => {
        const { expectObservable } = helpers;

        const target = {
            addEventListener: (eventType: any, listener: any) => {
                timer(5, 2, testScheduler)
                    .pipe(
                        mapTo('event'),
                        take(2),
                        concat(NEVER)
                    )
                    .subscribe(listener);
            },
            removeEventListener: (): void => void 0,
            dispatchEvent: (): void => void 0,
        };
        const e1 = fromEvent(target as any, 'click');
        const expected = '-----x-x---';
        expectObservable(e1).toBe(expected, {x: 'event'});
    });
});

But in the end you don’t really want to test the operator itself. The RxJS people have done that for you.

Exercise for the fromEvent operator 💪

Wrap the fromEvent operator to simplify testing for click events.