// ======================================================================== // SproutCore // copyright 2006-2008 Sprout Systems, Inc. // ======================================================================== /** @namespace Key-Value-Observing (KVO) simply allows one object to observe changes to a property on another object. It is one of the fundamental ways that models, controllers and views communicate with each other in a SproutCore application. Any object that has this module applied to it can be used in KVO-operations. This module is applied automatically to all objects that inherit from SC.Object, which includes most objects bundled with the SproutCore framework. You will not generally apply this module to classes yourself, but you will use the features provided by this module frequently, so it is important to understand how to use it. h2. Enabling Key Value Observing With KVO, you can write functions that will be called automatically whenever a property on a particular object changes. You can use this feature to reduce the amount of "glue code" that you often write to tie the various parts of your application together. To use KVO, just use the KVO-aware methods get() and set() to access properties instead of accessing properties directly. Instead of writing: {{{ var aName = contact.firstName ; contact.firstName = 'Charles' ; }}} use: {{{ var aName = contact.get('firstName') ; contact.set('firstName', 'Charles') ; }}} get() and set() work just like the normal "dot operators" provided by JavaScript but they provide you with much more power, including not only observing but computed properties as well. h2. Observing Property Changes You typically observe property changes simply by adding the observes() call to the end of your method declarations in classes that you write. For example: {{{ SC.Object.create({ valueObserver: function() { // Executes whenever the "Value" property changes }.observes('value') }) ; }}} Although this is the most common way to add an observer, this capability is actually built into the SC.Object class on top of two methods defined in this mixin called addObserver() and removeObserver(). You can use these two methods to add and remove observers yourself if you need to do so at run time. To add an observer for a property, just call: {{{ object.addObserver('propertyKey', targetObject, targetAction) ; }}} This will call the 'targetAction' method on the targetObject to be called whenever the value of the propertyKey changes. */ SC.Observable = { /** Manually add a new binding to an object. This is the same as doing the more familiar propertyBinding: 'property.path' approach. */ bind: function(toKey, fromPropertyPath) { var r = SC.idt.active ; var binding ; var props = { to: [this, toKey] } ; // for strings try to do default relay var pathType = $type(fromPropertyPath) ; if (pathType == T_STRING || pathType == T_ARRAY) { binding = this[toKey + 'BindingDefault'] || SC.Binding.From; binding = binding(fromPropertyPath) ; } else binding = fromPropertyPath ; // check the 'from' value of the relay. if it starts w/ // '.' || '*' then convert to a local tuple. var relayFrom = binding.prototype.from ; if ($type(relayFrom) == T_STRING) switch(relayFrom.slice(0,1)) { case '*': case '.': relayFrom = [this,relayFrom.slice(1,relayFrom.length)]; } if(r) bt = new Date().getTime(); binding = binding.create(props, { from: relayFrom }) ; this.bindings.push(binding) ; if (r) SC.idt.b1_t += (new Date().getTime()) - bt ; return binding ; }, /** didChangeFor makes it easy for you to verify that you haven't seen any changed values. You need to use this if your method observes multiple properties. To use this, call it like this: if (this.didChangeFor('render','height','width')) { // DO SOMETHING HERE IF CHANGED. } */ didChangeFor: function(context) { var keys = $A(arguments) ; context = keys.shift() ; var ret = false ; if (!this._didChangeCache) this._didChangeCache = {} ; if (!this._didChangeRevisionCache) this._didChangeRevisionCache = {}; var seen = this._didChangeCache[context] || {} ; var seenRevisions = this._didChangeRevisionCache[context] || {} ; var loc = keys.length ; var rev = this._kvo().revision ; while(--loc >= 0) { var key = keys[loc] ; if (seenRevisions[key] != rev) { var val = this.get(key) ; if (seen[key] !== val) ret = true ; seen[key] = val ; } seenRevisions[key] = rev ; } this._didChangeCache[context] = seen ; this._didChangeRevisionCache[context] = seenRevisions ; return ret ; }, // .......................................... // PROPERTIES // // Use these methods to get/set properties. This will handle observing // notifications as well as allowing you to define functions that can be // used as properties. /** Retrieves the value of key from the object. This method is generally very similar to using object[key] or object.key, however it supports both computed properties and the unknownProperty handler. *Computed Properties* Computed properties are methods defined with the property() modifier declared at the end, such as: {{{ fullName: function() { return this.getEach('firstName', 'lastName').compact().join(' '); }.property('firstName', 'lastName') }}} When you call get() on a computed property, the property function will be called and the return value will be returned instead of the function itself. *Unknown Properties* Likewise, if you try to call get() on a property whose values is undefined, the unknownProperty() method will be called on the object. If this method reutrns any value other than undefined, it will be returned instead. This allows you to implement "virtual" properties that are not defined upfront. @param key {String} the property to retrieve @returns {Object} the property value or undefined. */ get: function(key) { var ret = this[key] ; if (ret === undefined) { return this.unknownProperty(key) ; } else if (ret && (ret instanceof Function) && ret.isProperty) { return ret.call(this,key) ; } else return ret ; }, /** Sets the key equal to value. This method is generally very similar to calling object[key] = value or object.key = value, except that it provides support for computed properties, the unknownProperty() method and property observers. *Computed Properties* If you try to set a value on a key that has a computed property handler defined (see the get() method for an example), then set() will call that method, passing both the value and key instead of simply changing the value itself. This is useful for those times when you need to implement a property that is composed of one or more member properties. *Unknown Properties* If you try to set a value on a key that is undefined in the target object, then the unknownProperty() handler will be called instead. This gives you an opportunity to implement complex "virtual" properties that are not predefined on the obejct. If unknownProperty() returns undefined, then set() will simply set the value on the object. *Property Observers* In addition to changing the property, set() will also register a property change with the object. Unless you have placed this call inside of a beginPropertyChanges() and endPropertyChanges(), any "local" observers (i.e. observer methods declared on the same object), will be called immediately. Any "remote" observers (i.e. observer methods declared on another object) will be placed in a queue and called at a later time in a coelesced manner. *Chaining* In addition to property changes, set() returns the value of the object itself so you can do chaining like this: {{{ record.set('firstName', 'Charles').set('lastName', 'Jolley'); }}} @param key {String} the property to set @param value {Object} the value to set or null. @returns {this} */ set: function(key, value) { var func = this[key] ; var ret = value ; this.propertyWillChange(key) ; // set the value. if (func && (func instanceof Function) && (func.isProperty)) { ret = func.call(this,key,value) ; } else if (func === undefined) { ret = this.unknownProperty(key,value) ; } else ret = this[key] = value ; // post out notifications. this.propertyDidChange(key, ret) ; return this ; }, /** Sets the property only if the passed value is different from the current value. Depending on how expensive a get() is on this property, this may be more efficient. @param key {String} the key to change @param value {Object} the value to change @returns {this} */ setIfChanged: function(key, value) { return (this.get(key) !== value) ? this.set(key, value) : this ; }, /** Navigates the property path, returning the value at that point. If any object in the path is undefined, returns undefined. */ getPath: function(path) { var tuple = SC.Object.tupleForPropertyPath(path, this) ; if (tuple[0] === null) return undefined ; return tuple[0].get(tuple[1]) ; }, /** Navigates the property path, finally setting the value. @param path {String} the property path to set @param value {Object} the value to set @returns {this} */ setPath: function(path, value) { var tuple = SC.Object.tupleForPropertyPath(path, this) ; if (tuple[0] == null) return null ; tuple[0].set(tuple[1], value) ; return this; }, /** Convenience method to get an array of properties. Pass in multiple property keys or an array of property keys. This method uses getPath() so you can also pass key paths. @returns {Array} Values of property keys. */ getEach: function() { var keys = $A(arguments).flatten() ; var ret = []; for(var idx=0; idx 1) { var co = SC._ChainObserver.createChain(this,parts,func) ; co.masterFunc = func ; var chainObservers = kvo.chainObservers[key] || [] ; chainObservers.push(co) ; kvo.chainObservers[key] = chainObservers ; // otherwise, bind as a normal property } else { var observers = kvo.observers[key] = (kvo.observers[key] || []) ; var found = false; var loc = observers.length; while(!found && --loc >= 0) found = (observers[loc] == func) ; if (!found) observers.push(func) ; } }, removeObserver: function(key,func) { var kvo = this._kvo() ; // if the key contains a '.', this is a chained observer. key = key.toString() ; var parts = key.split('.') ; if (parts.length > 1) { var chainObservers = kvo.chainObserver[key] || [] ; var newObservers = [] ; chainObservers.each(function(co) { if (co.masterFunc != func) newObservers.push(co) ; }) ; kvo.chainObservers[key] = newObservers ; // otherwise, just like a normal observer. } else { var observers = kvo.observers[key] || [] ; observers = observers.without(func) ; kvo.observers[key] = observers ; } }, addProbe: function(key) { this.addObserver(key,logChange); }, removeProbe: function(key) { this.removeObserver(key,logChange); }, /** Logs the named properties to the console. @param propertyNames one or more property names */ logProperty: function() { var props = $A(arguments) ; for(var idx=0;idx 0) { var loc = dependents.length ; while(--loc >= 0) { var depKey = dependents[loc] ; _addDependentKeys(depKey) ; } } } ; // loop throught keys to notify. find all dependent keys as well. // note that this loops recursively. for(key in keySource) { if (!keySource.hasOwnProperty(key)) continue ; _addDependentKeys(key) ; } // Convert the found keys into an array for(key in allKeys) { if (!allKeys.hasOwnProperty(key)) continue ; keys.push(key) ; } var starObservers = kvo.observers['*'] ; // clear out changed to avoid recursion. var changed = kvo.changed ; kvo.changed = {} ; // notify all observers. var target = this ; loc = keys.length ; var notifiedKeys = {} ; // avoid duplicates. while(--loc >= 0) { key = keys[loc] ; observers = kvo.observers[key] ; if (!notifiedKeys[key]) { notifiedKeys[key] = key ; value = (allObservers || (!changed[key])) ? this.get(key) : changed[key]; if (starObservers) { observers = (observers) ? observers.concat(starObservers) : starObservers ; } if (observers) { oloc = observers.length ; var args = [target, key, value, this.propertyRevision] ; while(--oloc >= 0) { var observer = observers[oloc] ; SC.NotificationQueue.add(null, observer, args) ; } // while(oloc) } // if (observers) // notify self. if (this.propertyObserver != SC.Object.prototype.propertyObserver) { SC.NotificationQueue.add(this, this.propertyObserver, [null, target, key, value, this.propertyRevision]) ; } } } // while(--loc) SC.NotificationQueue.flush() ; } } ; // ........................................................................ // FUNCTION ENHANCEMENTS // // Enhance function. Object.extend(Function.prototype,{ // Declare a function as a property. This makes it something that can be // accessed via get/set. property: function() { this.dependentKeys = $A(arguments) ; this.isProperty = true; return this; }, // Declare that a function should observe an object at the named path. Note // that the path is used only to construct the observation one time. observes: function(propertyPaths) { this.propertyPaths = $A(arguments); return this; }, typeConverter: function() { this.isTypeConverter = true; return this ; }, /** Creates a timer that will execute the function after a specified period of time. If you pass an optional set of arguments, the arguments will be passed to the function as well. Otherwise the function should have the signature: {{{ function functionName(timer) }}} @param interval {Number} the time to wait, in msec @param target {Object} optional target object to use as this @returns {SC.Timer} scheduled timer */ invokeLater: function(target, interval) { if (interval === undefined) interval = 1 ; var f = this; if (arguments.length > 2) { var args =$A(arguments).slice(2,arguments.length); args.unshift(target); f = f.bind.apply(f, args) ; } return SC.Timer.schedule({ target: target, action: f, interval: interval }); } }) ; // ........................................................................ // OBSERVER QUEUE // // This queue is used to hold observers when the object you tried to observe // does not exist yet. This queue is flushed just before any property // notification is sent. SC.Observers = { queue: {}, addObserver: function(propertyPath, func) { // try to get the tuple for this. if (typeof(propertyPath) == "string") { var tuple = SC.Object.tupleForPropertyPath(propertyPath) ; } else { var tuple = propertyPath; } if (tuple) { tuple[0].addObserver(tuple[1],func) ; } else { var ary = this.queue[propertyPath] || [] ; ary.push(func) ; this.queue[propertyPath] = ary ; } }, removeObserver: function(propertyPath, func) { var tuple = SC.Object.tupleForPropertyPath(propertyPath) ; if (tuple) { tuple[0].removeObserver(tuple[1],func) ; } var ary = this.queue[propertyPath] ; if (ary) { ary = ary.without(func) ; this.queue[propertyPath] = ary ; } }, flush: function() { var newQueue = {} ; for(var path in this.queue) { var funcs = this.queue[path] ; var tuple = SC.Object.tupleForPropertyPath(path) ; if (tuple) { var loc = funcs.length ; while(--loc >= 0) { var func = funcs[loc] ; tuple[0].addObserver(tuple[1],func) ; } } else newQueue[path] = funcs ; } // set queue to remaining items this.queue = newQueue ; } } ; // ........................................................................ // NOTIFCATION QUEUE // // Property notifications are placed into this queue first and then processed // to keep the stack size down. SC.NotificationQueue = { queue: [], maxFlush: 5000, // max time you can spend flushing before we reschedule. _flushing: false, add: function(target, func, args) { this.queue.push([target, func, args]);}, flush: function(force) { if (this._flushing && !force) return ; this._flushing = true ; var start = new Date().getTime() ; var now = start ; var n = null ; while(((now - start) < this.maxFlush) && (n = this.queue.pop())) { var t = n[0] || n[1] ; n[1].apply(t,n[2]) ; now = Date.now() ; } this._flushing = false ; if (this.queue.length > 0) { SC.NotificationQueue.flush.invokeLater(SC.NotificationQueue, 1) ; } } } ;