Skip to content

Improving ergonomics of events with Observable #544

Open
@benlesh

Description

Observable has been at stage 1 in the TC-39 for over a year now. Under the circumstances we are considering standardizing Observable in the WHATWG. We believe that standardizing Observable in the WHATWG may have the following advantages:

  • Get Observable to web developers more quickly.
  • Allow for a more full-featured proposal that will address more developer pain points.
  • Address concerns raised in the TC-39 that there has not been sufficient consultation with implementers.

The goal of this thread is to gauge implementer interest in Observable. Observable can offer the following benefits to web developers:

  1. First-class objects representing composable repeated events, similar to how promises represent one-time events
  2. Ergonomic unsubscription that plays well with AbortSignal/AbortController
  3. Good integration with promises and async/await

Integrating Observable into the DOM

We propose that the "on" method on EventTarget should return an Observable.

partial interface EventTarget {
  Observable on(DOMString type, optional AddEventListenerOptions options);
};

[Constructor(/* details elided */)]
interface Observable {
  AbortController subscribe(Function next, optional Function complete, optional Function error);
  AbortController subscribe(Observer observer); // TODO this overload is not quite valid
  Promise<void> forEach(Function callback, optional AbortSignal signal);
 
  Observable takeUntil(Observable stopNotifier);
  Promise<any> first(optional AbortSignal signal);

  Observable filter(Function callback);
  Observable map(Function callback);
  // rest of Array methods
  // - Observable-returning: filter, map, slice?
  // - Promise-returning: every, find, findIndex?, includes, indexOf?, some, reduce
};

dictionary Observer { Function next; Function error; Function complete; };

The on method becomes a "better addEventListener", in that it returns an Observable, which has a few benefits:

// filtering and mapping:
element.on("click").
    filter(e => e.target.matches(".foo")).
    map(e => ({x: e.clientX, y: e.clientY })).
    subscribe(handleClickAtPoint);

// conversion to promises for one-time events
document.on("DOMContentLoaded").first().then(e => );

// ergonomic unsubscription via AbortControllers
const controller = element.on("input").subscribe(e => );
controller.abort();

// or automatic/declarative unsubscription via the takeUntil method:
element.on("mousemove").
    takeUntil(document.on("mouseup")).
    subscribe(etc => );

// since reduce and other terminators return promises, they also play
// well with async functions:
await element.on("mousemove").
    takeUntil(element.on("mouseup")).
    reduce((e, soFar) => );

We were hoping to get a sense from the whatwg/dom community: what do you think of this? We have interest from Chrome; are other browsers interested?

If there's interest, we're happy to work on fleshing this out into a fuller proposal. What would be the next steps for that?

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions