Skip to content

Instantly share code, notes, and snippets.

@Rich-Harris
Last active December 26, 2015 18:28
Show Gist options
  • Save Rich-Harris/7194138 to your computer and use it in GitHub Desktop.
Save Rich-Harris/7194138 to your computer and use it in GitHub Desktop.
Utility for making observable objects
// Usage
// =====
//
// Create an observable object:
//
// obj = makeObservable();
// obj.set( 'foo', 'bar' );
//
// observer = obj.observe( 'foo', function ( newFoo, oldFoo ) {
// alert( 'foo changed from ' + oldFoo + ' to ' + newFoo ); // alerts 'foo changed from undefined to bar'
// });
//
// obj.set( 'foo', 'baz' ); // alerts 'foo changed from bar to baz'
// obj.set( 'foo', 'baz' ); // does nothing
//
// obj.get( 'foo' ); // 'baz'
//
// observer.cancel();
// obj.set( 'foo', 'bop' ); // doesn't alert, we cancelled the observer
//
// obj.get( 'foo' ); // 'bop'
//
// Or take an existing object and make it observable (will overwrite any existing
// properties named `_attributes`, `_callbacks`, `set`, `get` or `observe`)
//
// makeObserveable( someObj );
(function ( global ) {
'use strict';
var isEqual, observable, makeObservable;
isEqual = function ( a, b ) {
// null is a special case as typeof null === 'object'
if ( a === null && b === null ) {
return true;
}
// if we're dealing with objects, return false as we don't know
// whether the contents have changed without doing a deep equality
// check.
if ( typeof a === 'object' || typeof b === 'object' ) {
return false;
}
return a === b;
};
observable = {
set: function ( key, value ) {
var map, oldValue, callbacks, i, len;
if ( typeof key === 'object' ) {
map = key;
for ( key in map ) {
if ( map.hasOwnProperty( key ) ) {
this.set( key, map[ key ] );
}
}
return;
}
oldValue = this.get( key );
if ( !isEqual( value, oldValue ) ) {
this._attributes[ key ] = value;
// notify observers
if ( !( callbacks = this._callbacks[ key ] ) ) {
return;
}
if ( callbacks.active ) {
return; // prevent infinite loops
}
callbacks.active = true;
len = callbacks.length;
for ( i = 0; i < len; i += 1 ) {
callbacks[i]( value, oldValue );
}
callbacks.active = false;
}
return this;
},
get: function ( key ) {
return this._attributes[ key ];
},
observe: function ( key, callback, options ) {
var map, callbacks, observers, i;
if ( typeof key === 'object' ) {
map = key;
options = callback;
observers = [];
for ( key in map ) {
if ( map.hasOwnProperty( key ) ) {
observers[ observers.length ] = this.observe( key, map[ key ], options );
}
}
return {
cancel: function () {
i = observers.length;
while ( i-- ) {
observers[i].cancel();
}
}
};
}
callbacks = this._callbacks[ key ] || ( this._callbacks[ key ] = [] );
callbacks[ callbacks.length ] = callback;
if ( !options || options.init === false ) {
callback( this.get( key ) );
}
return {
cancel: function () {
var index = callbacks.indexOf( callback );
if ( index !== -1 ) {
callbacks.splice( index, 1 );
}
}
};
}
};
makeObservable = function ( object ) {
object = object || {};
object._attributes = {};
object._callbacks = {};
object.set = observable.set;
object.get = observable.get;
object.observe = observable.observe;
return object;
};
// export as CommonJS module...
if ( typeof module !== 'undefined' && module.exports ) {
module.exports = makeObservable;
}
// ... or as AMD module...
else if ( typeof define !== 'undefined' && define.amd ) {
define( function () { return makeObservable; });
}
// ... or as browser global
else {
global.makeObservable = makeObservable;
}
}( typeof window !== 'undefined' ? window : this ));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment