JavaScript Observable: The Stage 2 Proposal for Event Streams

The TC39 Observable proposal (Stage 2) brings a standardised reactive event-stream primitive to the browser — similar to RxJS Observable but built into the platform. The DOM is also gaining a EventTarget.prototype.on() method that returns an Observable, replacing the common pattern of converting DOM events to streams with external libraries. You can experiment today with the WHATWG Observable polyfill.

Problem: WordPress admin JavaScript relies on callback-based addEventListener and removeEventListener for event streams — subscribing multiple listeners, composing filters, and disposing of listeners cleanly requires manual bookkeeping.

Solution: Use the TC39 Observable proposal (stage 2) — or the rxjs library today — to represent event streams as first-class objects. Subscribe with observable.subscribe({ next, error, complete }), compose with .filter(), .map(), and .takeUntil(), and dispose cleanly with the returned subscription object. Ideal for autocomplete inputs, search debouncing, and WebSocket streams.


The examples below show basic Observable creation, the EventTarget.on() API, operator chaining (filter, map, take), and a practical WordPress admin pattern for live-filtering a post table as the user types.


// ── 1. Basic Observable creation ─────────────────────────────────────────
const obs = new Observable( subscriber => {
    subscriber.next( 1 );
    subscriber.next( 2 );
    subscriber.next( 3 );
    subscriber.complete();
} );

obs.subscribe( {
    next:     v => console.log( 'value:', v ),
    error:    e => console.error( 'error:', e ),
    complete: () => console.log( 'done' ),
} );
// value: 1 / value: 2 / value: 3 / done

// ── 2. DOM events via EventTarget.on() (WHATWG proposal) ─────────────────
const btn = document.querySelector( '#my-button' );

// Returns an Observable — no need to manually add/remove listeners
btn.on( 'click' )
    .filter( e => ! e.target.disabled )
    .map( e => e.target.dataset.action )
    .subscribe( action => handleAction( action ) );

// ── 3. Operator chaining ─────────────────────────────────────────────────
const first5Clicks = document.querySelector( 'body' )
    .on( 'click' )
    .filter( e => e.target.matches( '.post-row' ) )
    .map( e => e.target.closest( 'tr' ).dataset.postId )
    .take( 5 );   // automatically unsubscribes after 5 events

first5Clicks.subscribe( id => console.log( 'Post clicked:', id ) );

// ── 4. Live post-table filter in WordPress admin ──────────────────────────
const searchInput = document.querySelector( '#post-search-input' );
const rows        = document.querySelectorAll( '#the-list tr' );

if ( searchInput ) {
    searchInput.on( 'input' )
        .map( e => e.target.value.toLowerCase().trim() )
        .subscribe( query => {
            rows.forEach( row => {
                const text = row.textContent.toLowerCase();
                row.hidden = query.length > 1 && ! text.includes( query );
            } );
        } );
}

// ── 5. Cleanup: subscription returns an object with unsubscribe ───────────
const subscription = btn.on( 'click' ).subscribe( handler );
// Later:
subscription.unsubscribe();   // removes the event listener automatically


NOTE: The TC39 Observable API is not yet in any browser natively — the EventTarget.on() method is part of the WHATWG DOM Observable proposal which is separate from (but related to) the TC39 Signals proposal; use the observable-polyfill npm package for production experimentation and plan to migrate to the native API when it ships.