UNPKG

southbay

Version:

Cocoa-inspired key-value coding, binding, and observing for node

367 lines (362 loc) 10.8 kB
require('./polyfills.js'); var southbay = require('./base.js'); /** * The KEYVALUEOBSERVING options ('KVO') used in message dispatching. NEW * Indicates that a passed exports.Change Dictionary should contain the new value of an obseved key * @type {Number} */ exports.NEW = 1; /** * Indicates that a passed exports.Change Dictionary should contain the old value of an obseved key * @type {Number} */ exports.OLD = 2; /** * Base is an Object that implements our Cocoa inspired * key-value-[coding|binding|observing] methods * @param {Object} model An optional data model * @constructor */ exports.Base = function(model) { this._data_ = model || {}; this._proposedValue_ = void 0; this._bindings_ = {}; this._proposedKeyPath_ = ''; this._proposedObject_ = null; this._proposedBinding_ = ''; this._isBinding_ = false; this._observers_ = {}; this._isSettingObserver_ = false; this._proposedChange_ = null; this._proposedObserver_ = null; this._proposedOptions_ = null; }; /** * Extend a southbay class with instance methods * @param {Object} instance An optional set of instance methods * @param {Object=} model An optional data model * @returns {Object} The assembled Object */ exports.Base.extend = function(instance, klass) { var o = southbay.inherits(this, instance, klass); o.extend = this.extend; return o; }; /** * In order to be notified of changes to a property, an observing object * must register with the object to be observed. Begins a chain * @param {Object} obj Who will be doing the observing * @returns {Object} this * @see southbay.Base.forKeyPath */ exports.Base.prototype.addObserver = function(obj) { this._isSettingObserver_ = true; this._proposedObserver_ = obj; return this; }; /** * Begin the chain in which this object will bind a method that will * be called by another object when a specific key(path) is modified * @param {String} binding * @return {Object} this */ exports.Base.prototype.bind = function(binding) { this._isBinding_ = true; this._proposedBinding_ = binding; return this; }; /** * A helper method that abstracts the responsibility of identifying * and calling bindings when `key` or `keypath` are set in KVB objects * @private */ exports.Base.prototype._callBound_ = function(path) { var i, curr, arr = this._bindings_[path]; for(i = 0; i < arr.length; i++) { curr = arr[i]; // shortest path is no options if(!curr.options) curr.binding(this._proposedValue_); else { // handle falsy _proposedValue_ if(!this._proposedValue_) { // this will need to change if more options are added curr.binding(curr.options.placeholder || this._proposedValue_); } else { // there is a _proposedValue_. Pass it through a transformer if present // if the transformer results in a falsy val, check for the placeholder curr.binding(curr.options.transformer ? curr.options.transformer( this._proposedValue_) || curr.options.placeholder : this._proposedValue_); } } } }; /** * The change dictionary passed from the object with the keypath * being observed * @param {Object} dict * @returns {object} this */ exports.Base.prototype.change = function(dict) { this._proposedChange_ = dict; return this; }; /** * For the Observable class objects, context is the method to invoke * once a notification has been recieved * @param {String} context A method to call */ exports.Base.prototype.context = function(method) { if(this._isSettingObserver_) { var c = { object: this._proposedObserver_, options: this._proposedOptions_, context: method }; // a keypath may have many observers if(this._proposedKeyPath_ in this._observers_) { this._observers_[this._proposedKeyPath_].push(c); } else { this._observers_[this._proposedKeyPath_] = [c]; } this._isSettingObserver_ = false; this._proposedObserver_ = null; this._proposedKeyPath_ = ''; this._proposedOptions_ = null; } else { // execute the specified response to a notification // passes in path, object and change dict this[method](this._proposedKeyPath_, this._proposedObject_, this._proposedChange_); this._proposedKeyPath_ = ''; this._proposedObject_ = null; this._proposedChange_ = null; } }; /** * The key for a value being stored in *this* object's key-value-store via * the setValue() method * Checks for any registered bindings or observers and messages them * if present * @param {String} key The name of the key * @see southbay.Base.setValue */ exports.Base.prototype.forKey = function(key) { // observers? if(this._observers_[key]) { this._notifyObservers_(key); } this._data_[key] = this._proposedValue_; // bindings? if(this._bindings_[key]) { this._callBound_(key); } this._proposedValue_ = void 0; }; /** * Traverse the keypath and get each object * (or make blank ones) eventually setting the _proposedValue_ * at the end of the path * Checks for any registered bindings or observers and messages them * if present * @param {string} path The keypath to traverse when setting a value */ exports.Base.prototype.forKeyPath = function(path) { // setting an observer? if(this._isSettingObserver_) { this._proposedKeyPath_ = path; return this; } // observers? if(this._observers_[path]) { this._notifyObservers_(path); } var key, curr = this._data_, p; p = path.split('.'); for (key; p.length && (key = p.shift());) { if(!p.length) { curr[key] = this._proposedValue_; } else if (curr[key]) { curr = curr[key]; } else { curr = curr[key] = {}; } } // bindings? if(this._bindings_[path]) { this._callBound_(path); } this._proposedValue_ = void 0; }; /** * A helper method that abstracts the responsibility of identifying * and calling observers when `key` or `keypath` are set in Observable objects * @private */ exports.Base.prototype._notifyObservers_ = function(path) { var i, j, opts, arr = this._observers_[path]; for(i = 0; i < arr.length; i++) { // handle the options, build a change dict if desired opts = arr[i].options; if(opts) { this._proposedChange_ = {}; // TODO other settings for kind? this._proposedChange_.Kind = southbay.SETTING; for(j = 0; j < opts.length; j++) { // NOTE use cases ATM are only OLD and NEW switch(opts[j]) { case southbay.NEW: this._proposedChange_.New = this._proposedValue_; break; case southbay.OLD: this._proposedChange_.Old = this.valueForKeyPath(path); break; default: console.log(opts[j] + " currently unsupported"); break; } } } arr[i].object.observeValueForKeyPath(path).ofObject( this).change(this._proposedChange_).context(arr[i].context); // clear out the change object that we made this._proposedChange_ = null; } }; /** * An observer must posses this method in order to respond to changes * to an observed object[keypath]. Called when the value at keypath changes * @param {String} path * @returns {Object} this */ exports.Base.prototype.observeValueForKeyPath = function(path) { this._proposedKeyPath_ = path; return this; }; /** * Continuing the chain along the observer notification * @param {Object} obj * @returns {Object} this */ exports.Base.prototype.ofObject = function(obj) { this._proposedObject_ = obj; return this; }; /** * One of a few methods serving multiple purposes in southbay. * Any binding optons are passed in a dictionary, different from the options * used for an observer. Valid observer values are the KEYVALUEOBSERVING types. * Can be sent a falsy value if no change object is desired back * @param {...number|object} KEYVALUEOBSERVING types or a dict * @returns {undefined|object} */ exports.Base.prototype.options = function(dict) { if(this._isBinding_) { var b = { binding: this[this._proposedBinding_].bind(this), options: dict || null }; // a keypath may have many bindings if(this._proposedKeyPath_ in this._proposedObject_._bindings_) { this._proposedObject_._bindings_[this._proposedKeyPath_].push(b); } else { this._proposedObject_._bindings_[this._proposedKeyPath_] = [b]; } this._isBinding_ = false; this._proposedObject_ = null; this._proposedKeyPath_ = ''; } else { if(arguments.length > 0) { this._proposedOptions_ = Array.prototype.slice.call(arguments, 0); } return this; } }; /** * The value that is going to be set for a key in *this* object's key-value-store. * This method is used by chaining it to the forKey() method. Argument can be of * any valid JS type * * @example * Foo.setValue({bar:true}).forKey('baz'); * * @param {*} value The value to be set, can be falsy * @returns {Object} this */ exports.Base.prototype.setValue = function(value) { this._proposedValue_ = value; return this; }; /** * Invokes setValue:forKey for each key value pair * @param {Object} dict The keys and values to set */ exports.Base.prototype.setValuesForKeys = function(dict) { for(var p in dict) { if(dict.hasOwnProperty(p)) this.setValue(dict[p]).forKey(p); } }; /** * The object that this object will create a binding with * @param {Object} obj * @returns {Object} this */ exports.Base.prototype.toObject = function(obj) { this._proposedObject_ = obj; return this; }; /** * Remove a binding present on this object * @param {String} binding The name of the binding */ exports.Base.prototype.unbind = function(binding) { delete this._bindings_[binding]; }; /** * Returns the value associated with a key in *this* objects key-value-store * @param {String} key The name of the key * @returns The value associated with the key or undefined if not found */ exports.Base.prototype.valueForKey = function(key) { return this._data_[key]; }; /** * Extract a value located at key-path relative to this objects * key-value-store * @param {String} path The key in the form of a dot-delimited path * @returns the value at keypath or undefined if not found */ exports.Base.prototype.valueForKeyPath = function(path) { var key, curr = this._data_, p; p = path.split('.'); for (key; p.length && (key = p.shift());) { if(!p.length) { return curr[key]; } else { curr = curr[key]; } } return curr; }; /** * Assembles and returns dict object of key:value pairs for each * passed in key * @param {array} arr An array of keys * @returns {object} */ exports.Base.prototype.valuesForKeys = function(arr) { var i, len, dict = {}; for (i = 0, len = arr.length; i < len; i++) { dict[arr[i]] = this.valueForKey(arr[i]); } return dict; }; /** * The keypath to a value on <toObject> that this object wishes * to synchronize with * @param {String} path * @returns {Object} this */ exports.Base.prototype.withKeyPath = function(path) { this._proposedKeyPath_ = path; return this; };