Observe_evented
A Javascript class that makes Array.observe and Object.observe easy to use.
Observe_evented's specialty is to split the batch of changes returned by the native API into atomic and consistent events. It also provides several options that make them easier to handle.
Test it on this jsFiddle page.
This library has no dependency. It works in Object.observe compatible environments like Node 0.11.13+ and Chrome 36+, or other environments with a shim that will emulate Array/Object.observe. For Object.observe, you can checkout Massimo Artizzu's "Object-observe" shim.
Object.observe is a proposed feature in the draft of ECMAScript 7. As the standard itself may still evolve, this library may change accordingly. You can read more about it here.
Available under MIT license on GitHub and Npm.
NOTE: examples on this page may produce different results if a shim is used instead of a natively compatible browser.
Quick reference guide
CREATE AN OBSERVER
// `object` can be an Array or an Object.var observer = observe_evented;
LISTEN TO CHANGES
// `eventType` is one event type or several space-separated event types,// which may be namespaced (see below).// `name` is optional, to filter the changes on a property name. It may be a// string or an array of strings.// `handler` is called at each change with a single event as first argument.observer;
REMOVE A LISTENER
observer;// or, to stop the listener only for a given event type:// (note: space-separated event types is also possible and// namespaces as well, see below)observer; // Note: queued events for the current object will immediately// (ie synchronously) be emitted when you remove a listener.
or, to remove several listeners:
// stop all listeners on the observer for a given event type:// (note: space-separated event types is also possible)observer;// or, to stop all listeners of the observer altogether:observer;
USING NAMESPACES
// namespaces are supported in the jQuery fashion:observer; // to unbind only a specific event type namespaced handler:observer;// or, to remove any type event type handler having this namespace:observer;
TRIGGER EVENTS ON HANDLERS
// `name` may be an property name (or an index for arrays) of the// observed object or an array of property namesobserver;// or, to trigger the event on all values in the object/array:observer;// or directly provide an array of eventsobserver; // example of the last syntax:observer;
PAUSE AN OBSERVER
observer;
RESUME AN OBSERVER
observer;
REMOVE AN OBSERVER
observer;
EVENTS have these properties:
name: PropertyNameOrIndexOfChangedValue object: observedObjectOrArray type: TypeOfEvent // not present on add events oldValue: valueBeforeEvent // not present on remove events value: valueAfterEvent
and their types are usually add
, update
or remove
.
OPTIONS
See the options section for full reference.
var options = additionalEventTypes: multipleObservers: false shim: null output: batchOnly: false dropValues: false minimalEvents: false noUpdateEvents: false
Basic examples
Observe an object:
var basket = {} observer = observe_evented; observer ; // triggers an add eventbasketfruit = 'apple';// triggers an update eventbasketfruit = 'strawberry';// triggers a remove eventdelete basketfruit;
Observe an array (works the same way):
var basket = observer = observe_evented; observer ; // triggers an add eventbasket0 = 'apple';// triggers an update eventbasket0 = 'pear';// triggers an add eventbasket;// triggers two add eventsbasket;// triggers two remove events and two add eventsbasket;
A special note about the delete basket[0];
command : as this effectively sets the value of basket[0]
to null
and does not actually remove the cell at index 0, it is an update
event that will be triggered with event.value == null
. Use the splice
method to actually remove values from an array and consider stop using delete
with arrays.
Observe a specific property of an object
Quite often you'll be interested in the changes of a specific property of an object. That's when you should use the second parameter of observer.on
.
You may provide a property name or an array of property names:
var basket = {} observer = observe_evented; observer ; // triggers an add eventbasketfruit = 'apple';// triggers an update eventbasketfruit = 'strawberry';// triggers nothingbasketvegetable = 'carrot';// triggers an update eventbasketvegetable = 'bean';// triggers nothingbasketdrink = 'coke';
Please note that this second argument, while technically working on arrays too, will probably not produce the result you would expect and will be useless to most people.
Data-binding with Observe_evented
One of the ways to use the power of Object.observe is for data-binding. As soon as your object/array changes, you might want to reflect that change in the DOM. Here is an example with the jQuery syntax:
var basket = {} observer = observe_evented; observer; // these will be listed on our web pagebasketfruit = 'apple';basketvegetable = 'carrot';basketdrink = 'coke';
Great ! But sometimes you will have objects/arrays that will have been populated before the observer was set up:
// the basket is already full of goodsvar basket = fruit: 'apple' vegetable: 'carrot' drink: 'coke' observer = observe_evented; // Our existing goods will not be listed in the page as they were// added to the basket before this listener is boundobserver;
To solve this and initialize your page correctly, after you bound your handler with observer.on()
, just fire it by triggering "fake" add
events on every property of the basket:
observer;
Observe multiple objects or arrays from a single function
Just use a single handler on multiple observers:
var basket1 = basket2 = {} { var nb = eventobjectconstructor === Array ? 1 : 2; console; }; var observer1 = observe_evented observer2 = observe_evented; observer1;observer2; basket1;basket2fruit = 'watermellon';
Event types
By default, the events sent by Observe_evented can be of the following types :
add
, update
, remove
, batch
, reconfigure
, setPrototype
Notifiers also let you create more event types (read the article linked at the top of this page).
Options : optimize performances
An object of options can optionaly be provided in the call to observe_evented.observe()
.
Before explaining the options, note that Observe_evented :
- returns by default all events that happened on the object/array you observe,
- adds meaningful
event.value
properties that help you understand what happened all the way.
While this can be interesting in some cases and while debugging, you might want to work differently. The following options are here for that :
options.output.dropValues
Since generating the event.value
properties has a (generally) minor performance cost, you can choose not to generate them if you don't need them. Default: false
.
options.output.minimalEvents
Set this option to true
to get only the minimum number of events nessary to transition from the object/array as it was before modifications to its current state. This can effectively optimize your application by ignoring meaningless events. Example :
var basket = fruit: 'apple' observer = observe_evented; observer ; basketfruit = 'banana';basketfruit = 'mango';basketfruit = 'cherry';basketvegetable = 'carrot';basketvegetable = 'pea';basketmeat = 'pork';basketmeat = 'chicken';delete basketmeat; // Logged ://// The property "fruit" was updated, its value is "cherry"// A property "vegetable" was added, its value is "pea"
As you can see, Observe_evented sends events as if the object went directly from its original state to its final one, that is to say :
from { fruit: 'apple' }
to { fruit: 'cherry', vegetable: 'pea' }
No events were sent for the intermediary states of basket.fruit
and basket.vegetable
. Since basket.meat
does exist before the modifications nor after, no events are sent about that either.
Other options
options.additionalEventTypes
If you need an object observer to return more than the standard change types, namely because you use notifiers, you may provide them to this option as an array. Default: empty array.
options.multipleObservers
See the dedicated "Create multiple observers on a same object/array" section below. Default: false
.
options.shim
Should you wish to use a shim that does not directly extend Object and Array prototype, you may provide an adapter for it via this option. You must provide an object that has Array
and Object
properties which will expose observe
, unobserve
and deliverChangeRecords
methods.
options.output.batchOnly
If you prefer to work on batched events (see the section below) and do not need them to be triggered individually, set this option to true
. Only the batch
events will then be triggered. Default: false
.
options.output.noUpdateEvents
Semantically, updating a value in an array/object is not the same thing as removing it and adding its replacement at the same index/key. However, the end result is the same. Furthermore, a browser using a shim won't even be able to make the difference. For the sake of consistency, this is why you may set this option to true
to prevent update
events and replace them by a remove
event followed by an add
event. Default: false
observe_evented.setDefaultOptions()
This method lets you modify the default options for all future calls, like for example:
observe_evented; // all observers created from now on will now only emit events without// `event.value` being set, and `update` events will be converted.
Advanced: create multiple observers on a same object/array
By default, Observe_evented creates only one observer per object/array for simplicity and improved performances, even if you call observe_evented.observe()
multiple times on it.
That is to say:
var basket = fruit: 'apple' observer1 = observe_evented observer2 = observe_evented; // this logs `true`console;
However, please note that when you call observe_evented.observe()
a second time, the options provided in the first call will be lost and replaced by the options of the second call (or the default options if not provided).
In case you really need to create a second observer on the object/array, for example because you want to work with a different set of options in parallel, you may force this by setting the multipleObservers
option to true
.
As a result:
var basket = fruit: 'apple' observer1 = observe_evented observer2 = observe_evented; // this logs `false`console;
Please note that when you use several observers on an object/array, all events will be fired on an observer, and then all events will be fired on the next, etc.
Advanced: the short syntax of .observe()
Some people might be happy to know that they can actually provide a listener as third argument to observe_evented.observe()
, that is to say:
var observer = observe_evented;
This is equivalent to:
var observer = observe_evented;observer;
Which has for effect that your handler will be called for every single event fired on the observer.
Advanced: pause, resume and remove observers
When the observer is paused by using its the disable
method, Object.unobserve()
is actually called. Its configuration and listeners however are kept in case you ever want to resume observing by calling its enable
method.
When you call the destroy
method of the observer, the object/array is unobserved and references to its configuration and listeners are definitively deleted.
When called, the destroy
method will synchronously deliver any queued events before destruction. If you wish to proceed to the destruction asynchronously at the end of the current microtask, you may call it with true
as the first argument.
Example:
var basket = fruit: 'apple' observer = observe_evented; observer; // triggers an update eventbasketfruit = 'banana';observer;// triggers nothingbasketfruit = 'mango';observer;// triggers an update eventbasketfruit = 'cherry';observer;// triggers nothingbasketfruit = 'pear';
Note: since the listener called by Object.observe
is actually not the same before disabling and after enabling again, the events are coming to your handlers in two distinct batches.
Advanced: deliverChangeRecords
This methods is directly proxied by Observe_evented on the currently observed object, without the need for you to provide any arguments.
When calling this method, all queued events will be immediately fired (synchronously), instead of having to wait for the end of the current microtask (asynchronously).
var basket = fruit: 'apple' observer = observe_evented; observer;
Advanced : Manage the events in batches
As you know, events are sent asynchronously and in batches by the browser to Observe_evented. If you didn't know, read the notice section below as it's something you must understand to avoid surprises.
So, sometimes you might find useful to get all events at once in your handler function, instead of having it called with only one event at a time. No problem.
You will be able to get the batch of atomic events computed by Observe_evented and the batch of raw events sent by the native Object/Array.observe function.
You can get these by listening to batch
events.
var basket = observer = observe_evented; observer; basket0 = 'apple';basket;basket;// delete should be avoided on arrays, it's only for the exampledelete basket0;basket;
...will log:
// Note: "object" has been collapsed, its value is ["strawberry", "apple", "pear"]// all along. "name": 0 "object": ... "type": "add" "value": "apple" "name": 1 "object": ... "type": "add" "value": "pear" "name": 0 "object": ... "type": "add" "value": "banana" "name": 1 "object": ... "type": "add" "value": "cherry" "type": "update" "object": ... "name": 0 "oldValue": "banana" "value": null "name": 0 "object": ... "oldValue": null "type": "remove" "name": 0 "object": ... "oldValue": "cherry" "type": "remove" "name": 0 "object": ... "type": "add" "value": "strawberry" "type": "splice" "object": ... "index": 0 "removed": "addedCount": 1 "type": "splice" "object": ... "index": 1 "removed": "addedCount": 1 "type": "splice" "object": ... "index": 0 "removed": "addedCount": 2 "type": "delete" "object": ... "name": "0" "oldValue": "banana" "type": "splice" "object": ... "index": 0 "removed": null "cherry" "addedCount": 1
Important notice
Events are sent asynchronously by the Object/Array.observe methods unless you call the deliverChangeRecords
method.
This means that events will be sent after all commands in the current microtask have been run.
This means that when you do:
var basket = fruit: 'apple' ; observer = ; observer; basketfruit = 'pear';basketfruit = 'grape';basketfruit = 'strawberry';
...the following will be logged:
The fruit property was updated its value changed to pearBut its real current value is strawberryThe fruit property was updated its value changed to grapeBut its real current value is strawberryThe fruit property was updated its value changed to strawberryBut its real current value is strawberry
That also explains why event.object
reflects the state of the object as it is after all changes have been made.
For more information, read the article linked at the top of this page.