Observable content changes
This JavaScript package, suitable for browsers and Node.js, provides a system
for synchronously observing content changes to arrays, objects, and other
instances.
These observers have a common, composable style, expose their internal state for
debugging, and reuse state tracking objects to reduce garbage collection.
- Changes can be captured before or after they are made.
- The last argument of a change notification is the object observed, so a
single handler can service multiple objects.
- Handler methods can return a child observer object, which will be implicitly
cancelled before the next change, so observers can be stacked.
- Does not alter the Array base type, but promotes array instances to an
ObservableArray when they are observed.
Installation
npm install --save pop-observe@2
Examples
Observing the length of an array.
var array = [];
var observer = O.observePropertyChange(array, "length", function (length) {
expect(length).toBe(1);
});
array.push(10);
observer.cancel();
Observing a property of an ordinary object.
var object = {weight: 10};
var observer = O.observePropertyChange(object, "weight", function (weight) {
expect(weight).toBe(20);
});
object.weight = 20;
observer.cancel();
Observing values at indexes.
var array = [];
var change;
var handler = {
handlePropertyChange: function (plus, minus, index, object) {
change = {
plus: plus,
minus: minus,
index: index,
object: object
};
}
};
var observer0 = O.observePropertyChange(array, 0, handler);
var observer1 = O.observePropertyChange(array, 1, handler);
var observer2 = O.observePropertyChange(array, 2, handler);
array.set(0, 10);
expect(change).toEqual({plus: 10, minus: undefined, index: 0, object: array});
array.set(0, 20);
expect(change).toEqual({plus: 20, minus: 10, index: 0, object: array});
array.set(1, 20);
expect(change).toEqual({plus: 20, minus: undefined, index: 1, object: array});
Mirroring arrays.
var O = require("pop-observe");
var swap = require("pop-swap");
var array = [];
var mirror = [];
var observer = O.observeRangeChange(array, function (plus, minus, index) {
swap(mirror, index, minus.length, plus);
});
array.push(1, 2, 3);
array.shift();
array.pop();
expect(mirror).toEqual([2]);
observer.cancel();
Tracking an array with a plain object.
var O = require("pop-observe");
var array = [];
var object = {};
var observer = O.observeMapChange(array, function (plus, minus, index, type) {
if (type === "delete") {
delete object[index];
} else {
object[index] = plus;
}
});
array.push(1, 2, 3);
expect(object).toEqual({0: 1, 1: 2, 2: 3});
array.splice(1, 1);
expect(object).toEqual({0: 1, 1: 3});
Observing a property of a property.
Note that the cancel method gets rid of observers on a.b and b.c.
Note that the b.c observer gets canceled every time b changes.
var O = require("pop-observe");
var a = {b: {c: 10}};
var value;
var observer = O.observePropertyChange(a, "b", function (b) {
value = b.c;
return O.observePropertyChange(b, "c", function (c) {
value = c;
});
});
a.b = {c: 20};
expect(value).toBe(20);
a.b.c = 30;
expect(value).toBe(30);
observer.cancel();
Change notification arguments
- Property change observers dispatch (plus, minus, name, object) change
notifications when the value of a specific, named property changes.
Each observer sees the old value (minus), new value (plus), property name
(name), and the object (object) such that a single property change handler
can service multiple observers.
Observing a property of an ordinary object replaces that property with a
getter and setter.
- Range change observers dispatch (plus, minus, index, object) change
notifications when ordered values are removed (captured in a minus array),
then added (captured in the plus array), at a particular index.
Each handler also receives the object observed, so a single handler can
service multiple observers.
- Map change observers dispatch (plus, minus, key, object) change
notifications when the value for a specific key in a map has changed.
Behavior on Arrays
- This library does not alter plain JavaScript arrays or the Array prototype.
- Observing an array transforms that array into an ObservableArray either by
subverting its prototype or adding properties directly to the instance.
Observable arrays have additional swap and set methods.
- Observing any property change on an array transforms that array into an
observable array and property changes are dispatched for the "length" or any
value by its index.
- Observing range changes on an array transforms that array into an observable
array and all of its methods produce these change notifications.
- Observing any map change on an array transforms the array into an observable
array and map changes are dispatched for changes to the value at the given
index.
Custom types
Arbitrary constructors can mix in or inherit the ObservableObject type to
support the observable interface directly and do not need to provide any further
support.
var inherits = require("util").inherits;
var ObservableObject = require("pop-observe/observable-object");
function Custom() {}
inherits(Custom, ObservableObject);
Arbitrary constructors can mix in or inherit the ObservableRangeChange type and
must explicitly dispatch change notifications when range change observers are
active.
var inherits = require("util").inherits;
var ObservableRange = require("pop-observe/observable-range");
function Custom() {}
inherits(Custom, ObservableRange);
Customer.prototype.unshift = function unshift(value) {
if (this.dispatchesRangeChanges) {
this.dispatchRangeWillChange([value], [], 0);
}
if (this.dispatchesRangeChanges) {
this.dispatchRangeChange([value], [], 0);
}
};
This library does not provide any map implementations but provides the
ObservableMap for any to inherit or mix in.
var inherits = require("util").inherits;
var ObservableMap = require("pop-observe/observable-map");
function Custom() {}
inherits(Custom, ObservableMap);
Customer.prototype.delete = function delete(key) {
var old = this.get(key);
if (!old) {
return;
}
if (this.dispatchesMapChanges) {
this.dispatchMapWillChange("delete", key, undefined, old);
}
if (this.dispatchesMapChanges) {
this.dispatchMapChange("delete", key, undefined, old);
}
};
All of thse can be mixed by copying the properties from their prototypes.
var ObservableObject = require("pop-observe/observable-object");
var owns = Object.prototype.hasOwnProperty;
for (var name in ObservableObject.prototype) {
if (owns.call(ObservableObject.prototype, name)) {
Customer.prototype[name] = ObservableObject.prototype[name];
}
}
Interface
Each type of observer provides before and after methods for observation and
manual dispatch.
For properties, manual dispatch is necessary when a property is hidden behind a
getter and a setter if the value as returned by the getter changes without the
setter ever being invoked.
Arrays require manual dispatch only if the value at a given index changes
without invoking an array mutation method.
For this reason, observable arrays have a set(index, value)
method.
All ranged and map collections must implement manual dispatch when their
dispatchesRangeChanges
or dispatchesMapChanges
properties are true.
Object property change observers
-
observePropertyChange(object, handler, note, capture) -> Observer
-
observePropertyWillChange(object, handler, note) -> Observer
-
dispatchPropertyChange(object, name, plus, minus, capture)
-
dispatchPropertyWillChange(object, name, plus, minus)
-
makePropertyObservable(object, name)
-
preventPropertyObserver(object, name)
Range change observers
- observeRangeChange(object, handler, note, capture) -> Observer
- observeRangeWillChange(object, handler, note) -> Observer
- dispatchRangeChange(object, plus, minus, index, capture)
- dispatchRangeWillChange(object, plus, minus, index)
Map change observers
- observeMapChange(object, handler, note, capture) -> Observer
- observeMapWillChange(object, handler, note) -> Observer
- dispatchMapChange(object, type, key, plus, minus, capture)
- dispatchMapWillChange(object, type, key, plus, minus)
Observer objects in general
- Observer.prototype.cancel();
Handlers
Handlers may be raw functions, or objects with one or more handler methods.
Observers for different kinds of changes and before and after changes call
different methods of the handler object based on their availability at the time
that the observer is created.
For example, observePropertyWillChange(array, "length", handler)
will create a
property observer that will delegate to the
handler.handleLengthPropertyWillChange(plus, minus, key, object)
method, or
just that generic handler.handlePropertyWillChange(plus, minus, key, object)
method if the specific method does not exist.
- observable object, property observers (plus, minus, key, object)
- after change
- specific: handlePropertyNameChange
- general: handlePropertyChange
- before change
- specific: handlePropertyNameWillChange
- general: handlePropertyWillChange
Range changes do not operate on a given property name, but the
observeRangeChange(handler, name, note, capture)
method allows you to give the
range change a name, for example, the name of the array observed on a given
object.
var handler = {
handleValuesRangeChange: function (plus, minus, index, object) {
}
};
var observer = repetition.values.observeRangeChange(handler, "values");
repetition.values.push(10);
observer.cancel();
- observable range (plus, minus, index, object)
- after change
- specific: handleNameRangeChange
- general: handleRangeChange
- before change
- specific: handleNameRangeWillChange
- general: handleRangeWillChange
Likewise, observeMapChange(handler, name, note, capture)
accepts a name for a
specific handler method.
- observable map (plus, minus, key, object)
- after change
- specific: handleNameMapChange
- general: handleMapChange
- before change
- specific: handleNameMapWillChange
- general: handleMapWillChange
Observers
Observers are re-usable objects that capture the state of the observer.
Most importantly, they provide the cancel
method, which disables the observer
and returns it to a free list for the observer methods to re-use.
They are suitable for run-time inspection of the state of the observer.
They also carry an informational note
property, if the caller of the observe
method provided one.
This is intended for use by third parties to provide additional debugging
information about an observer, for example, where the observer came from.
- PropertyChangeObserver
- object
- propertyName
- observers
- handler
- handlerMethodName
- childObserver
- note
- capture
- value
- RangeChangeObserver
- object
- name
- observers
- handler
- handlerMethodName
- childObserver
- note
- capture
- MapChangeObserver
- object
- name
- observers
- handler
- handlerMethodName
- childObserver
- note
- capture
Implementing observability
The pop-observe/observable-object
, pop-observe/observable-range
, and
pop-observe/observable-map
modules export mixable or prototypically
inheritable constructors.
Objects that inherit the observable interface must then dispatch change and will
change notifications if they are being observed, in all of their methods that
change their content.
-
ObservableObject.observePropertyChange(object, handler, note, capture)
-
ObservableObject.observePropertyWillChange(object, handler, note)
-
ObservableObject.dispatchPropertyChange(object, name, plus, minus, capture)
-
ObservableObject.dispatchPropertyWillChange(object, name, plus, minus)
-
ObservableObject.getPropertyChangeObservers(object, name, capture)
-
ObservableObject.getPropertyWillChangeObservers(object, name)
-
ObservableObject.makePropertyObservable(object, name)
-
ObservableObject.preventPropertyObserver(object, name)
-
ObservableObject.prototype.observePropertyChange(handler, note, capture)
-
ObservableObject.prototype.observePropertyWillChange(handler, note)
-
ObservableObject.prototype.dispatchPropertyChange(name, plus, minus, capture)
-
ObservableObject.prototype.dispatchPropertyWillChange(name, plus, minus)
-
ObservableObject.prototype.getPropertyChangeObservers(name, capture)
-
ObservableObject.prototype.getPropertyWillChangeObservers(name)
-
ObservableObject.prototype.makePropertyObservable(name)
-
ObservableObject.prototype.preventPropertyObserver(name)
-
ObservableRange.prototype.observeRangeChange(handler, name, note, capture)
-
ObservableRange.prototype.observeRangeWillChange(handler, name, note)
-
ObservableRange.prototype.dispatchRangeChange(handler, name, note, capture)
-
ObservableRange.prototype.dispatchRangeWillChange(handler, name, note)
-
ObservableRange.prototype.makeRangeChangesObservable()
-
ObservableMap.prototype.observeMapChange(handler, name, note, capture)
-
ObservableMap.prototype.observeMapWillChange(handler, name, note)
-
ObservableMap.prototype.dispatchMapChange(type, key, plus, minus, capture)
-
ObservableMap.prototype.dispatchMapWillChange(type, key, plus, minus)
-
ObservableMap.prototype.makeMapChangesObservable()
Copyright and License
Copyright (c) 2015 Motorola Mobility, Montage Studio, Kristopher Michael Kowal,
and contributors.
All rights reserved.
BSD 3-Clause license.