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
CreateListeningProxy
|
Main function for creating listening proxies on objects
|
ListeningProxyFactory
|
Factory for creating listening proxies
|
EVENT_TYPE_BEFORE_CHANGE
|
Event type for before change listeners
"beforeChange"
|
EVENT_TYPE_AFTER_CHANGE
|
Event type for after change listeners
"afterChange"
|
EVENT_TYPE_GET_PROPERTY
|
Event type for get property listeners
"getProperty"
|
EVENT_TYPE_EXCEPTION_HANDLER
|
Event type for exception handler listeners (i.e. exceptions within other listeners)
"exceptionHandler"
|
EVENT_TYPE_GET_TREEWALKER
|
Event type for get treewalker listeners
"getTreewalker"
|
SYMBOL_IS_PROXY
|
Symbol used to determine if an object is a listening proxy
|
SYMBOL_PROXY_TARGET
|
Symbol for obtaining the underlying target object of a listening proxy
|
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
});
BeforeChangeEvent
|
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
});
AfterChangeEvent
|
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
});
GetPropertyEvent
|
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;
});
ExceptionHandlerEvent
|
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