observer.js
is a tiny Javascript library to adopt observer pattern in a project with minimal impact on existing codebase.
It allows transparent interaction between modules without the need to modify modules itself.
It tracks an execution state of observers to avoid infinite loop on cyclic configurations.
Import lib with Bower package manager:
bower install observer-js
or by download dist/observer.min.js
and import in project with direct include
<script src='observer.min.js'></script>
or with a module loader.
All you have to do is declare a function as observable:
var myObservedFun = (function() {}).observable();
or overwrite an existing reference to a function with observable version:
myObj.myObservedFun = myObj.myObservedFun.observable();
Now you can set observers, also with a chaining notation:
var firstObserver = function() { console.log('fistObserver execution!') };
var secondObserver = function() { console.log('secondObserver execution!') };
myObservedFun.addObserver(firstObserver)
.addObserver(secondObserver);
Observers removal is supported. One by one:
myObservedFun.removeObserver(firstObserver);
or all in one shot:
myObservedFun.removeAllObservers();
That's all!
observer.js
supports more complex situations: read next sections.
observer.js
supports complex configurations with conditional executions and parameters retrieval for observers.
To bind observer execution to a context you can clearly pass the binded version: .addObserver(myObserver.bind(myObj))
.
Otherwise you can set the execution context by a property, passing an object to addObserver()
with fn
and context
.
myObservedFun.addObserver({
fn: myObserver,
context: myObj
})
The first method is discouraged, especially if same function is used more than one time as observer because it may lead to a fail in state-checking.
This because func != func.bind(context)
and, more important, func.bind(context) != func.bind(context)
.
In a cyclic configuration of observers it can lead to an infinite loop.
Observers execution can be conditionated to conditions provided during observer binding.
To define conditions you have to pass an object to addObserver()
, with observer function and conditions definition:
myObservedFun.addObserver({
fn: firstObserver,
conditions: {} | Function
})
conditions
can be set with an object or function (single condition) or an array (multiple conditions).
A single condition is defined in two ways:
myObservedFun.addObserver({
fn: firstObserver,
conditions: myFun
})
Value returned from execution is checked. The example corresponds to if (myFun())
.
with at least one parameter firstParam
to be checked before execution:
myObservedFun.addObserver({
fn: firstObserver,
conditions: { firstParam: 'nested.object.paramName' }
})
In general you can define a condition as a comparison of two parameters:
you have to set two parameters (firstParam
, secondParam
) and a comparison operator (operator
).
As parameter you can set:
- value returned from observed: you have to set parameter to
OBS_OBSERVED_RETURNED
- primitive value: you have to specify type attribute (
firstType
forfirstParam
orsecondType
forsecondParam
) asOBS_PRIMITIVE
- "stringed" reference of a variable (dot-notation is supported): if reference is not in global scope you have to specify it with
firstScope
orsecondScope
- reference to a function
As operator you can optionally set: "==="
, "!=="
, "=="
, "!="
, "<"
, "<="
, ">"
, ">="
. Default value: "=="
.
A condition with only one parameter (firstParam
) will be checked by if (firstParam)
.
All possible properties of condition object: firstParam
, secondParam
, operator
, firstType
, secondType
, firstScope
, secondScope
Example with value of variable myObj.nestedObj.myVar
compared with value returned from anotherObj.myFun
function:
conditions: {
firstParam: 'nestedObj.myVar',
firstScope: myObj
operator: '<=',
secondParam: anotherObj.myFun
}
Example with value returned from observed function compared with null
(primitive value):
conditions: {
firstParam: OBS_OBSERVED_RETURNED,
operator: '===',
secondParam: null,
secondType: OBS_PRIMITIVE
}
Boolean expressions to compare multiple single conditions is supported.
To use boolean operators you have to set conditions
as array, where first element is one of "AND"
, "OR"
, "NOT"
.
Subsequent elements can be single conditions or array itself (recursive). If operator is "NOT"
, in the array can be only another element, the condition.
Example condition: (A && B && (C || !D || E))
will be:
conditions: ["AND", A, B, ["OR", C, ["NOT", D], E]]
where A
, ..., E
are single condition objects defined like above.
It is possible to pass parameters to observers at execution time.
To define parameters you have to pass an object to addObserver()
, with observer function and parameters definition:
myObservedFun.addObserver({
fn: firstObserver,
params: []
})
params
(optional) is an array with elements that will be passed to function.
Three types of objects in array:
- Function, executed to retrieve params
- Primitive values
OBS_OBSERVED_RETURNED
to pass value returned from observer
Example:
params: [ getterFun, Math.PI, "name", OBS_OBSERVED_RETURNED, 42]
corresponds to: observer(getterFun(), Math.PI, "name", ret, 42)
where ret = observed()
Since observers can be in their turn observed it is possible to have a cyclic-graph configuration. In this case loop is avoided by a mechanism of state-checking and observers execution is triggered via a dfs exploration of graph.
Configuration example:
var a = (function() { }).observable();
var b = (function() { }).observable();
var c = (function() { }).observable();
var d = (function() { }).observable();
var e = (function() { }).observable();
var f = (function() { }).observable();
var g = (function() { }).observable();
a.addObserver(b).addObserver(c);
b.addObserver(d).addObserver(e);
c.addObserver(b);
d.addObserver(c);
e.addObserver(g).addObserver(f);
f.addObserver(e);
above configuration corresponds to:
In this case execution flows are:
a(); // -> b(), d(), c(), e(), g(), f()
d(); // -> c(), b(), e(), g(), f()
f(); // -> e(), g()
In case of more observers on an observed, their execution occurs in order in which they were added.
observer.js
saves two variables in global scope:OBS_OBSERVED_RETURNED
andOBS_PRIMITIVE
myFun != myFun.observable()