ProxyListener.js Download
ProxyListener.js is a library for observing changes for any data types of object properties, such as object, array, function, etc .
It uses Proxy API and Object.defineProperty method, you can detect changes easily by creating a listener.
By using proxy-polyfill, it can works with IE9+, Chrome5+, Firefox4+, Opera11.6+, Node.js.
-
- Basic
- Advanced
- new ProxyListener().proxyListen(object,address,funSet,propSet).subscribe(callback(passing))
- subscribe(callback(passing))
- Delete & Redefine callback function
- Create multiple listeners at once
- new ProxyListener().proxyListenGroup(object, addressArray, funSet,propSet).subscribeGroup (callback(passing))
- Combine with Rxjs
- Compatible with target's getter/setter
-
- Using ProxyListener.js with HTML script tag
- Avoid infinite Loop
- Detect changes or execution for asynchronous function type target
- Deeply Detect changes for array type target
- Deeply detect changes for object type target
- Detect view data changes in Vue component
- Detect instance's methods's execution
- Application in Composite pattern
<script src="proxylistener.min.js" type="text/javascript"></script>
npm install proxylistenerjs
import ProxyListener from '.proxylistenerjs' //import es2015 version
import ProxyListener from '.proxylistenerjs/proxylistener.min.js' //import compatible version for browser
import ProxyListener from '.proxylistenerjs/proxylistener.node.min.js'//import compatible version for node.js
var ProxyListener = require("proxylistenerjs/proxylistener.js") //require es2015 version
var ProxyListener = require("proxylistenerjs/proxylistener.min.js") //require compatible version for browser
var ProxyListener = require("proxylistenerjs/proxylistener.node.min.js")//require compatible version for node.js
var pListener = new ProxyListener()
var object = {
word: 'word' //the listening target is word property
}
var objectListener = pListener.proxyListen(object,'word')
var objectSubscription = objectListener.subscribe(x => {
console.log('detect changes');
})
object.word = 'new'
/**
* console output: detect changes
*/
-
object: :
object
: necessary — target's parent object. -
address:
string
: necessary — target's location. (example: 'path', 'path1/path2') -
funSet:
object
: optional — settings for function type target.-
sub options:
-
exePos:
string
(default value: 'after') — position for callback function executing . --values:
---'before': callback function execute before the target function's execution.
---'after': callback function execute after the target function's execution.
---'both': callback function execute before and after the target function's execution.
-
funRepListen:
boolean
(default value: false) — whether to execute callback function after the target function is replaced by another function. -
isAsync:
boolean
(default value: false) — whether to execute callback function asynchronously after the target function's execution when target function is asynchronous.
-
-
-
propSet:
object
:optional — general listening settings.-
sub options:
-
thisArgs:
object
— the this argument for validator function's execution. -
validator:
function:boolean
— a function for intercepting the target's changes or the target function's execution by returning a boolean.Listener will pass an object when validator function is excuted, the object contains:
---locatePath : the location of the target's changing property.
---method: the way that the target changes.
---val: the data that will be added to the target's changing property.
For more detail, see the description of Passing Object's Properties
-
isTrigger:
boolean
(default value: false) — whether to execute listener's callback function when validator function return false. -
change:
object
— setting for listener passing object when callback function execute.- sub options:
- isPassOldValue:
boolean
(default value: false) — whether to pass old value when execute callback function. Notice: The true value setting may lower performance. - defaultValue:
string
(default value: undefined) — custom value for listener passing when callback function execute.
- isPassOldValue:
- sub options:
-
deepListenLv :
number
|string
(default value: 0) — the additional levels for enumerable properties listening for the target , by default, 1 level nested properties can be listened for the target. --values:
---1~?
number
: add additional levels for the target's properties listening. ---'max'
string
: listen all level of nested enumerable properties of the target -
coverSet:
object
— settings for the listener's reaction when the listener is covered, by default, a target's listener can be covered by creating duplicately.- sub options:
- isCanCover:
boolean
(default value: true) — whether the listener can be covered, if not, error function will execute when the listener is being covered. - errFunc:
function
— a function for throwing error whenisCanCover
's value is false.
- isCanCover:
- sub options:
-
funcListenSet:
object
— settings for the listener's reaction when function type properties execute.- sub options:
-
listenOn:
boolean
(default value: false) -
exePos:
string
(default value: 'after') — position for callback function executing . --values:
---'before': callback function execute before the target's function type properties's execution.
---'after': callback function execute after the target's function type properties's execution.
---'both': callback function execute before and after the target function's execution.
-
isAsync:
boolean
(default value: false) — whether to execute callback function asynchronously after the target's asynchronous function type properties's execution. -
instanceMethodOn :
boolean
|object
(default value: false) — whether to execute callback function partially or completely when the function type property is a instance's method. --values:
---true: detect all the execution of instances's methods within the target.
---{include: ['ClassOne', {class: 'ClassTwo', method: ['methodOne']}], notInclude:['ClassThree', {class: 'ClassFour', method: ['methodOne']}]}
-- sub options:
--- include: setting for specific class type instance or instance's methods can be detected after execution. For detecting all methods of a specific class type instance, use
{include:['ClassOne']}
, for detecting part of methods of a specific class type instance, use{include:[ {class: 'ClassFour', method: ['methodOne']}]}
. --- notInclude: setting for all the execution of instances's methods can be detected except for a few specific class type instances. The way of setting is the same as the
include
property. When you set a specific class in bothinclude
andnotInclude
property, the specific class's setting innotInclude
property will be invalid.Notice: The instanceMethodOn setting may lower performance.
-
- sub options:
-
-
When callback function execute, listener will pass a parameters object . According to listener options and situation of target's changes, the passing object's properties will change responsively:
-
passing.glob:
object
|function
|array
|string
|null
|boolean
|number
|undefined
— access the primitive target which without detecting changes. -
passing.locatePath :
string
— whatever a property is existing or new in target object or target array, listener will pass the location of this property when callback function execute. -
passing.method :
string
— ('assign'|'update'|'function'|array method's name) — the way that the target changes. --values:
---'assign': target or target's properties is replaced.
---'update' : detect changes of properties of target's array type properties.
---'function': target's function type properties
---array method's name: detect changes of array type target or target's array type properties with array methods.
-
passing.oldValue :
object
|function
|array
|string
|null
|boolean
|number
|undefined
— whenisPassOldValue
's value is true, listener will pass the past status of target or target's properties. -
passing.newValue :
object
|function
|array
|string
|null
|boolean
|number
|undefined
— listener will pass the undated status of target or target's properties.
When create listener and define callback function separately, you can keep the listener and delete callback function with unsubscribe function.
When you need to detect changes for the same target again, just redefine callback function without creating a listener duplicately.
objectSubscription.unsubscribe()
var objectReSubscription = objectListener.subscribe(x => {
console.log('detect changes again');
})
If you want to create multiple listeners quickly, you can use proxyListenGroup
function after initialization.
new ProxyListener().proxyListenGroup(object, addressArray, funSet,propSet).subscribeGroup (callback(passing))
- addressArray:
array
— array of targets's locations. - other options are equal to proxyListen function.
use unsubscribeGroup
function to delete multiple listener's callback function
objectSubscription.unsubscribeGroup()
You can add a Observable callback function to listener using Rxjs.
import { Subject, of } from 'rxjs'
import ProxyListener from '.proxylistenerjs'
import { concatMap } from 'rxjs/operators';
// initialize with rxjs
var pListener = new ProxyListener(Subject)
var control = {
listen: {
word: 'word',
}
}
// setting callback function with Observable methods
pListener.proxyListen(control, 'listen')
.pipe(concatMap(x => {
console.log(`print ${x.locatePath} changes once`);
return of(x)
}))
.subscribe(x => {
console.log(`print ${x.locatePath} changes twice`);
})
// trigger listener's detection
control.listen.word = 'new'
/* console output:
* print listen/word changes once
* print listen/word changes twice
*/
If taeget has getter/setter before creating listener, the listener will keep them rather than covering them.
<script type="text/javascript" src="proxylistener.min.js"></script>
<script>
var pListener = new ProxyListener()
var object = {
word: 'word' //the listening target is word property
}
var objectListener = pListener.proxyListen(object, 'word')
var objectSubscription = objectListener.subscribe(function (x) {
console.log('detect changes');
})
object.word = 'new'
</script>
If you change the object, array, function type target in it's callback function but don't want to execute the callback function again , just access target's glob property to change target
var pListener = new ProxyListener()
var object = {
target: { word: 'word' }
}
var objectListener = pListener.proxyListen(object, 'target')
var objectSubscription = objectListener.subscribe(function (x) {
// use target's glob function to return primitive target
object.target.glob().word = 'change'
// use passing object's glob property to access primitive target
x.glob.word = 'second change'
// callback function won't execute when the target changes using glob property
console.log('detect cahnges');
})
object.target.word = 'new'
console.log(object.target.word); //second change
/**
* console output:
* detect cahnges
* second change
*/
var pListener = new ProxyListener()
var object = {
asyncFunctionOne: function () {
console.log('asyncFunctionOne');
return new Promise((resolve, reject) => {
resolve();
}).then(x => {
console.log(`asyncFunctionOne is still executing`);
})
},
asyncFunctionTwo: function () {
console.log('asyncFunctionTwo');
return new Promise((resolve, reject) => {
resolve();
}).then(x => {
console.log(`asyncFunctionTwo is still executing`);
})
}
}
var oneListener = pListener.proxyListen(object, 'asyncFunctionOne')
var twoListener = pListener.proxyListen(object, 'asyncFunctionTwo', {
isAsync: true
})
//execute callback function synchronously
oneListener.subscribe(x => {
console.log(`asyncFunctionOne's execution is finish`);
})
//execute callback function asynchronously
twoListener.subscribe(x => {
console.log(`asyncFunctionTwo's execution is finish`);
})
object.asyncFunctionTwo()
object.asyncFunctionOne()
/* console output:
* asyncFunctionTwo
* asyncFunctionTwo's execution is finish
* asyncFunctionOne
* asyncFunctionTwo is still executing
* asyncFunctionOne is still executing
* asyncFunctionOne's execution is finish
*/
var pListener = new ProxyListener()
var object = {
array: [[0, 1, 2, 3], [3, 2, 1], [0]]
}
var arrayListener = pListener.proxyListen(object, 'array', {
deepListenLv: "max"
})
var arraySubscription = arrayListener.subscribe(x => {
console.log('location is' + ' ' + x.locatePath);
console.log('method is' + ' ' + x.method);
})
// change target by array method
object.array[0].reverse()
// change target by property assignment
object.array[3] = [1, 2, 3]
/* console output:
* location is array/0
* method is reverse
* location is array/3
* method is update
*/
var pListener = new ProxyListener()
var object = {
targetZero: {
func: function () {
console.log('this is targetZero');
},
array: [[0, 1, 2, 3], [3, 2, 1], [0]]
},
targetMax: {},
targetMultiple: {
targetOne: {
word: 'One'
},
targetTwo: {
word: 'Two'
}
}
}
var lvZeroListener = pListener.proxyListen(object, 'targetZero', {
validator: function (x) {
if (x.val.content) {
console.log('validator return false');
return false
} else {
console.log('validator return true');
return true
}
}
})
var lvMaxListener = pListener.proxyListen(object, 'targetMax', {
deepListenLv: "max"
})
lvZeroListener.subscribe(x => {
console.log('location is' + ' ' + x.locatePath)
console.log('method is' + ' ' + x.method);
})
lvMaxListener.subscribe(x => {
console.log('location is' + ' ' + x.locatePath)
console.log('method is' + ' ' + x.method);
})
var multipleListener = pListener.proxyListenGroup(object, ['targetMultiple/targetOne', 'targetMultiple/targetTwo'])
multipleListener.subscribeGroup(x => {
console.log('location is' + ' ' + x.locatePath)
console.log('execute the same callback function');
})
object.targetZero.func()
object.targetZero.array.reverse()
object.targetZero.nested = { content: 'nested' }
// new property can't be added to target because validator return false
object.targetMax.nested = { content: 'nested' }
console.log(object.targetZero.nested); // undefined
// new property can be added to target because validator return true
object.targetZero.nested = { word: 'nested' }
// callback function can be triggered because taget's listen level is max
object.targetMax.nested.content = 'new';
// callback function can't be triggered because taget's listen level is 0
object.targetZero.nested.word = 'new';
object.targetMultiple.targetOne.word = 'new';
//execute the same callback function as targetOne
object.targetMultiple.targetTwo.word = 'new';
/* console output:
* this is targetZero
* validator return false
* location is targetMax/nested
* method is assign
* undefined
* validator return true
* location is targetZero/nested
* method is assign
* location is targetMax/nested/content
* method is assign
* location is targetMultiple/targetOne/word
* execute the same callback function
* location is targetMultiple/targetTwo/word
* execute the same callback function
*/
The listener keep view data's getter/setter so that it can keep the ability to update DOM automatically.
<div id="app">
<div v-for="array in items">
<div v-for="item in array">
<p>{{item.num}}</p>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: [[{ num: 1.1 }, { num: 1.2 }, { num: 1.3 }],
[{ num: 2.1 }, { num: 2.2 }, { num: 2.3 }],
[{ num: 3.1 }, { num: 3.2 }, { num: 3.3 }]]
},
created() {
var pListener = new ProxyListener()
var object = {
target: { word: 'word' }
}
var objectListener = pListener.proxyListen(this, 'items', { deepListenLv: "max" })
var objectSubscription = objectListener.subscribe(function (x) {
if (x.locatePath) {
alert(`detect ${x.locatePath} change, method is ${x.method}`);
} else {
console.log(`detect target change, method is ${x.method}`);
}
})
},
mounted() {
this.items.reverse()
this.$nextTick(x => {
var that = this
setTimeout(function() {
that.items[0].push({ num: 1.4 }) // DOM updates asynchronously
}, 1000);
})
}
})
</script>
By customizing instance's methods's listening settings, the listener can detect instance's methods's execution partially or completely.
var pListener = new ProxyListener()
class Origin {
constructor() {}
output() {
console.log('this is Class instance');
}
}
class Class extends Origin {
constructor() {
super()
}
}
var instance = new Class()
var object = {
instance: instance
}
pListener.proxyListen(object, 'instance', { deepListenLv: 'max', funcListenSet: { listenOn: true, instanceMethodOn: { include: ['Class'] } } })
.subscribe(function (x) {
console.log('location is' + ' ' + x.locatePath);
console.log('method is' + ' ' + x.method);
})
// listener can detect instance's methods's execution through prototype chain
object.instance.output()
/* console output:
* this is Class instance
* location is instance/output
* method is function
*/
You can execute specific marco tree nodes's commands rather than executing all nodes's commands.
var pListener = new ProxyListener()
// organize the macroTree
var object = {
macroTree: {
command1: {
node: 'command1',
children: {
'command1.1': {
node: 'command1.1'
},
'command1.2': {
node: 'command1.2'
}
}
},
command2: {
node: 'command2'
},
command3: {
node: 'command3'
}
}
}
class Create {
constructor(para) {
this.para = para
}
execute() {
console.log(`command execution from node path: ${this.para}`);
}
}
var funcObj = {}
function loopAll(macroTree) {
for (var key in macroTree) {
macroTree[key]['node'] = key
if (macroTree[key]['children']) {
loopAll(macroTree[key]['children'])
}
}
}
var macrotreeListerner = pListener.proxyListen(object, 'macroTree', { deepListenLv: "max" })
macrotreeListerner.subscribe(x => {
if (!funcObj[x.locatePath]) {
funcObj[x.locatePath] = new Create(x.locatePath)
} else {
// execute specific marco tree nodes's commands
if (x.locatePath.search('command1') > -1) {
funcObj[x.locatePath].execute()
}
}
})
// add functions to funcObj
loopAll(object.macroTree)
// execute specific node command's in macro commands
loopAll(object.macroTree)
/* console output:
* command execution from node path: macroTree/command1/node
* command execution from node path: macroTree/command1/children/command1.1/node
* command execution from node path: macroTree/command1/children/command1.2/node
*/
// dynamic marcoTree
var pListener = new ProxyListener()
class Node {
constructor(name, type) {
if (type !== 'main') {
this.node = name
this.children = {}
}
}
main(node) {
this[node['node']] = node
}
add(node) {
this.children[node['node']] = node
}
execute(path) {
console.log(`command execution from node path: ${path}`);
}
}
function loopAll(macroTree) {
for (var key in macroTree) {
macroTree[key]['node'] = key
if (macroTree[key]['children']) {
loopAll(macroTree[key]['children'])
}
}
}
// organize the macroTree
var object = { macroTree: new Node(null, 'main') }
var command1 = new Node('command1')
var command2 = new Node('command2')
var command3 = new Node('command3')
command1.add(new Node('command1.1'))
command1.add(new Node('command1.2'))
object.macroTree.main(command1)
object.macroTree.main(command2)
object.macroTree.main(command3)
var macrotreeListerner = pListener.proxyListen(object, 'macroTree', {
validator: function (x) {
if (x.val.node){
// when adding new node after creating a listener
x.val.added = 'new node'
}
},
deepListenLv: "max"
})
macrotreeListerner.subscribe(x => {
// execute specific marco tree nodes's commands
if (x.locatePath.search('command1') > -1 && x.newValue.execute) {
x.newValue.execute(x.locatePath)
}
if (!x.newValue.execute) {
// output an inform when adding new node after creating a listener
console.log('new node is added');
}
})
loopAll(object.macroTree)
// add new node after creating a listener
object.macroTree.command1.add(new Node('command1.3'))
/* console output:
* command execution from node path: macroTree/command1/node
* command execution from node path: macroTree/command1/children/command1.1/node
* command execution from node path: macroTree/command1/children/command1.2/node
* new node is added
*/
The testing files are in __tests__
folder and the result's review is here.