listening-proxy

JavaScript Deep Proxy that can have listeners added


Keywords
proxy, deep, listening, listener, observable, observe, observer, object observe, object.observe, javascript, listen, listeners
License
Apache-2.0
Install
npm install listening-proxy@1.0.4

Documentation

LOGO

npm GitHub

Overview

A Javascript deep proxy that can have listeners added (an event listener based alternative to both Proxy and the defunct Object.observe()).

Example

import { CreateListeningProxy, EVENT_TYPE_BEFORE_CHANGE, EVENT_TYPE_AFTER_CHANGE } from 'listening-proxy.js';
 
const myOriginalObject = {
    foo: 'bar',
    buzz: false
};
 
const myProxyObject = CreateListeningProxy(myOriginalObject);

// add some listeners...
myProxyObject.addListener(EVENT_TYPE_BEFORE_CHANGE, evt => {
    console.log('First listener', evt);
    if (evt.action === 'set' && evt.property === 'buzz' && evt.value === true) {
        // stop other 'beforeChange' listeners firing...
        evt.stopPropagation();
    }
});
myProxyObject.addListener(EVENT_TYPE_BEFORE_CHANGE, evt => {
    console.log('Second listener', evt);
    if (evt.action === 'set' && evt.property === 'foo') {
        // stop the property actually being set...
        // (will also stop any 'afterChange' listeners firing)
        evt.preventDefault();
    }
});
myProxyObject.addListener(EVENT_TYPE_AFTER_CHANGE, evt => {
    console.log('Third listener', evt);
});
 
// now make some changes to our object...
myProxyObject.foo = 'blah';
console.log('Foo should still be bar', myProxyObject.foo);
 
myProxyObject.buzz = true;

Advantages over normal Proxy

  • Uses a single handler - that many listeners can hook into
  • addListener() style similar to addEventListener()
  • Deep listening (i.e. deep proxy)
    • Add listeners at any level in the object tree
    • Objects in tree shared in other trees fire all listeners
  • Familiar event.preventDefault() and event.stopPropogation() within listeners
  • beforeChange and afterChange events
  • getProperty events - allow 'simulating' properties/functions that aren't really there (without messing with prototype)
  • Multiple event listeners - with propagation prevention
  • Proxy listen on special objects (with event notification of all setter/change methods)
    • Typed Arrays
    • Date
    • Set
    • Map
    • and class instances

Reference

Exports

Function CreateListeningProxy Main function for creating listening proxies on objects
Class ListeningProxyFactory Factory for creating listening proxies
String EVENT_TYPE_BEFORE_CHANGE Event type for before change listeners
"beforeChange"
String EVENT_TYPE_AFTER_CHANGE Event type for after change listeners
"afterChange"
String EVENT_TYPE_GET_PROPERTY Event type for get property listeners
"getProperty"
String EVENT_TYPE_EXCEPTION_HANDLER Event type for exception handler listeners (i.e. exceptions within other listeners)
"exceptionHandler"
String EVENT_TYPE_GET_TREEWALKER Event type for get treewalker listeners
"getTreewalker"
Symbol SYMBOL_IS_PROXY Symbol used to determine if an object is a listening proxy
Symbol SYMBOL_PROXY_TARGET Symbol for obtaining the underlying target object of a listening proxy
Symbol SYMBOL_PROXY_LISTENERS Symbol for obtaining the underlying proxy listeners of a listening proxy

Creating the listening proxy

import { CreateListeningProxy } from 'listening-proxy.js';
 
let obj = {
    'foo': 'bar'
};
let objProxy = CreateListeningProxy(obj);

Determining if an object is a listening proxy

import { CreateListeningProxy, SYMBOL_IS_PROXY } from 'listening-proxy.js';
 
let obj = {
    'foo': 'bar'
};
let objProxy = CreateListeningProxy(obj);
 
// see if each is a proxy...
console.log( obj[SYMBOL_IS_PROXY] );  // expect output: undefined
console.log( myProxy[SYMBOL_IS_PROXY] );  // expect output: true

Obtaining the underlying target object of a listening proxy

import { CreateListeningProxy, SYMBOL_PROXY_TARGET } from 'listening-proxy.js';

let obj = {
    'foo': 'bar'
};
let objProxy = CreateListeningProxy(obj);

// get the target...
let target = myProxy[SYMBOL_PROXY_TARGET];

Creating a listening proxy on an object that is already a listening proxy?

Don't Panic! You do not need to check if the object is already a listening proxy - creating a listening proxy on an object that is already a listening proxy will just return the original listening proxy.

import { CreateListeningProxy } from 'listening-proxy.js';

let obj = {
    'foo': 'bar'
};

let objProxy = CreateListeningProxy(obj);

let anotherProxy = CreateListeningProxy(objProxy);

console.log(objProxy === anotherProxy);  // output: true

Adding listeners when creating a listening proxy

There are two ways to achieve this...

import { CreateListeningProxy, EVENT_TYPE_BEFORE_CHANGE, EVENT_TYPE_BEFORE_CHANGE } from 'listening-proxy.js';

let obj = {
    'foo': 'bar'
};

let objProxy = CreateListeningProxy(obj)
    .addListener(EVENT_TYPE_BEFORE_CHANGE, evt => {
        console.log('Before change', evt.snapshot);
    })
    .addListener(EVENT_TYPE_AFTER_CHANGE, evt => {
      console.log('After change', evt.snapshot);
    });

objProxy.foo = 'baz';

or...

import { CreateListeningProxy, EVENT_TYPE_BEFORE_CHANGE, EVENT_TYPE_BEFORE_CHANGE } from 'listening-proxy.js';

let obj = {
    'foo': 'bar'
};

let objProxy = CreateListeningProxy(obj,
    {
        'eventType': EVENT_TYPE_BEFORE_CHANGE,
        'listener': evt => {
          console.log('Before change', evt.snapshot);
        } 
    },
    {
        'eventType': EVENT_TYPE_AFTER_CHANGE,
        'listener': evt => {
            console.log('After change', evt.snapshot);
        }
    }
);

objProxy.foo = 'baz';

Can I add listeners to different parts of an object tree?

Yes!...

import { CreateListeningProxy, EVENT_TYPE_BEFORE_CHANGE, EVENT_TYPE_BEFORE_CHANGE } from 'listening-proxy.js';

let obj = {
    foo: {
        bar: {
            baz: {
                qux: true
            }
        } 
    }
};

let objProxy = CreateListeningProxy(obj)
    .addListener(EVENT_TYPE_BEFORE_CHANGE, evt => {
          console.log('Before change', evt.snapshot);
      })
      .addListener(EVENT_TYPE_AFTER_CHANGE, evt => {
        console.log('After change', evt.snapshot);
      });

let sub = objProxy.foo.bar;
sub.addListener(EVENT_TYPE_BEFORE_CHANGE, evt => {
    console.log('Sub before change', evt.snapshot);
}).addListener(EVENT_TYPE_AFTER_CHANGE, evt => {
    console.log('Sub after change', evt.snapshot);
});

objProxy.foo.bar.baz.qux = false; // will fire all 4 event listeners!
sub.baz.qux = true; // will also fire all 4 event listeners!
// note that listeners added at different parts of the tree - the event .path property is relative!

Listeners & Event Reference


EVENT_TYPE_BEFORE_CHANGE

Listen for changes prior to them being enacted on the underlying target

Example:

import { CreateListeningProxy, EVENT_TYPE_BEFORE_CHANGE } from 'listening-proxy.js';

const obj = { foo: 'bar' };
const proxy = CreateListeningProxy(obj);

proxy.addListener(EVENT_TYPE_BEFORE_CHANGE, event => {
    // handle the 'event' as instance of BeforeChangeEvent 
});
Event BeforeChangeEvent
Event Event
Properties
Property Description
action
The action being performed - one of:
  • "set" when the value of a property (or array item) is being set
  • "deleteProperty" when a property is being deleted
  • the name of the method causing the change (e.g. if obj.splice() is called then this value would be "splice()")
arguments
If the change event is caused by a method call, this value will be the arguments that were passed to that method
If the change was not caused by a method call then this will be undefined
defaultPerformed
Whether the default action (on the underlying target) has already been performed
defaultPrevented
Whether the default action has been prevented
path
The path to the item being changed (excluding the actual property)
preventable
Whether this event is preventable (always true for this event type)
propagates
Whether this event propagates (always true for this event type)
propagationStopped
Whether propagation has been stopped on this event (i.e. no further listeners will receive this event)
Use the stopPropagation() method on this event to set this
property
The property being changed.
When a property of an object is being changed this value will be the name of the property being changed. When an item in an array is being changed this value will be the array index being changed.
When the change is due to a method this value will be undefined
proxy
The actual proxy object on which this event was fired
WARNING: Mutating this object within listeners will cause serious problems - like infinite recursive calls to listeners
snapshot
As the event propagates through multiple listeners it will be mutated - this property provides a snapshot object of the event that isn't mutated (useful for logging/debugging purposes)
target
The actual underlying object on which this event was fired
WARNING: Mutating this object within listeners will cause serious problems
type
The type of this event
For this event, returns EVENT_TYPE_BEFORE_CHANGE ("beforeChange")
value
The value being set.
If the change is caused by a method call then this will usually be undefined
wasValue
The value prior to being set.
If the change is caused by a method call then this will usually be undefined
Methods
performDefault() Call this method to perform the default change within the listener
This method can only be called once - subsequent calls by this or other listeners will be ignored. Calls to this method will also be ignored if the preventDefault() method has previously been called.
Note: Calling this method does not stop the after change event listeners being called.
preventDefault() Call this method to prevent the default change from occurring.
Note: Preventing the default change on a before change event will also stop after change event listeners being called (i.e. the change didn't happen!).
stopPropagation() Call this method to stop further propagation of this event to other listeners of the same type



EVENT_TYPE_AFTER_CHANGE

Listen for changes after they have been enacted on the underlying target

Example:

import { CreateListeningProxy, EVENT_TYPE_AFTER_CHANGE } from 'listening-proxy.js';

const obj = { foo: 'bar' };
const proxy = CreateListeningProxy(obj);

proxy.addListener(EVENT_TYPE_AFTER_CHANGE, event => {
    // handle the 'event' as instance of AfterChangeEvent 
});
Event AfterChangeEvent
Event Event
Properties
Property Description
action
The action being performed - one of:
  • "set" when the value of a property (or array item) is being set
  • "deleteProperty" when a property is being deleted
  • the name of the method causing the change (e.g. if obj.splice() is called then this value would be "splice()")
arguments
If the change event is caused by a method call, this value will be the arguments that were passed to that method
If the change was not caused by a method call then this will be undefined
path
The path to the item being changed (excluding the actual property)
preventable
Whether this event is preventable (always false for this event type)
propagates
Whether this event propagates (always true for this event type)
propagationStopped
Whether propagation has been stopped on this event (i.e. no further listeners will receive this event)
Use the stopPropagation() method on this event to set this
property
The property being changed.
When a property of an object is being changed this value will be the name of the property being changed. When an item in an array is being changed this value will be the array index being changed.
When the change is due to a method this value will be undefined
proxy
The actual proxy object on which this event was fired
WARNING: Mutating this object within listeners will cause serious problems - like infinite recursive calls to listeners
snapshot
As the event propagates through multiple listeners it will be mutated - this property provides a snapshot object of the event that isn't mutated (useful for logging/debugging purposes)
target
The actual underlying object on which this event was fired
WARNING: Mutating this object within listeners will cause serious problems
type
For this event, returns EVENT_TYPE_AFTER_CHANGE ("afterChange")
value
The value being set.
If the change is caused by a method call then this will usually be undefined
wasValue
The value prior to being set.
If the change is caused by a method call then this will usually be undefined
Methods
stopPropagation() Call this method to stop further propagation of this event to other listeners of the same type



EVENT_TYPE_GET_PROPERTY

Listen for all get property actions on an object (this includes gets for functions/methods)

Example:

import { CreateListeningProxy, EVENT_TYPE_GET_PROPERTY } from 'listening-proxy.js';

const obj = { foo: 'bar' };
const proxy = CreateListeningProxy(obj);

proxy.addListener(EVENT_TYPE_GET_PROPERTY, event => {
    // handle the 'event' as instance of GetPropertyEvent 
});
Event GetPropertyEvent
Event Event
Properties
Property Description
asAction
(see preventDefault() method)
defaultPrevented
Whether the default action has been prevented
defaultResult
The default result (returned value) for the get
firesBeforesAndAfters
(see preventDefault() method)
path
The path to the item being retrieved (excluding the actual property)
preventable
Whether this event is preventable (always true for this event type)
propagates
Whether this event propagates (always true for this event type)
propagationStopped
Whether propagation has been stopped on this event (i.e. no further listeners will receive this event)
Use the stopPropagation() method on this event to set this
property
The property being retrieved.
When a property of an object is being retrieved (or a method of an object) this value will be the name of the property/method being retrieved. When an item in an array is being retrieved this value will be the array index being retrieved.
proxy
The actual proxy object on which this event was fired
WARNING: Mutating this object within listeners will cause serious problems - like infinite recursive calls to listeners
result
The actual result (returned value) for the get
(see preventDefault() method)
snapshot
As the event propagates through multiple listeners it will be mutated - this property provides a snapshot object of the event that isn't mutated (useful for logging/debugging purposes)
target
The actual underlying object on which this event was fired
WARNING: Mutating this object within listeners will cause serious problems
type
For this event, returns EVENT_TYPE_GET_PROPERTY ("getProperty")
Methods
preventDefault(replacementResult [, firesBeforesAndAfters [, asAction]]) Calling this method prevents the default result of the get operation being returned
Arguments:
  • replacementResult 
    the replacement result
  • firesBeforesAndAfters 
    whether, if the replacement result is a function, before and after events should be fired
  • asAction 
    if before and after events are to be fired - the action that will be passed to those event listeners
stopPropagation() Call this method to stop further propagation of this event to other listeners of the same type



EVENT_TYPE_EXCEPTION_HANDLER

Listen for exceptions in other listeners.

By default, listening proxy 'swallows' any exceptions throwm/encountered within listeners (although they are still output as console errors). By adding an EVENT_TYPE_EXCEPTION_HANDLER listener such exceptions can be handled and, if required, surfaced.

Example:

import { CreateListeningProxy, EVENT_TYPE_EXCEPTION_HANDLER } from 'listening-proxy.js';

const obj = { foo: 'bar' };
const proxy = CreateListeningProxy(obj);

proxy.addListener(EVENT_TYPE_EXCEPTION_HANDLER, event => {
    // handle the 'event' as instance of ExceptionHandlerEvent
    // example to surface exception...
    throw event.exception;
});
Event ExceptionHandlerEvent
Event Event
Properties
Property Description
event
The original event that was being handled at the point the exception occurred
exception
The exception that occurred
handler
The handler function in which the exception occurred
preventable
Whether this event is preventable (always false for this event type)
propagates
Whether this event propagates (always true for this event type)
propagationStopped
Whether propagation has been stopped on this event (i.e. no further listeners will receive this event)
Use the stopPropagation() method on this event to set this
proxy
The actual proxy object on which this event was fired
WARNING: Mutating this object within listeners will cause serious problems - like infinite recursive calls to listeners
snapshot
As the event propagates through multiple listeners it will be mutated - this property provides a snapshot object of the event that isn't mutated (useful for logging/debugging purposes)
target
The actual underlying object on which this event was fired
WARNING: Mutating this object within listeners will cause serious problems
type
For this event, returns EVENT_TYPE_EXCEPTION_HANDLER ("exceptionHandler")
Methods
stopPropagation() Call this method to stop further propagation of this event to other listeners of the same type



Supported On

Node Chrome Firefox Safari iOS Edge Opera