1 // ========================================================================== 2 // Project: SproutCore Costello - Property Observing Library 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 // ........................................................................ 9 // CHAIN OBSERVER 10 // 11 12 // This is a private class used by the observable mixin to support chained 13 // properties. 14 15 // ChainObservers are used to automatically monitor a property several 16 // layers deep. 17 // org.plan.name = SC._ChainObserver.create({ 18 // target: this, property: 'org', 19 // next: SC._ChainObserver.create({ 20 // property: 'plan', 21 // next: SC._ChainObserver.create({ 22 // property: 'name', func: myFunc 23 // }) 24 // }) 25 // }) 26 // 27 SC._ChainObserver = function (property, root) { 28 this.property = property; 29 this.root = root || this; 30 }; 31 32 /** @private 33 This is the primary entry point. Configures the chain. 34 35 @param {String} path The property path for the chain. Ex. 'propA.propB.propC.@each.propD' 36 */ 37 SC._ChainObserver.createChain = function (rootObject, path, target, method, context) { 38 // First we create the chain. 39 var parts = path.split('.'), // ex. ['propA', 'propB', '@each', 'propC'] 40 root = new SC._ChainObserver(parts[0]), // ex. _ChainObserver({ property: 'propA' }) 41 tail = root; 42 43 for (var i = 1, len = parts.length; i < len; i++) { 44 tail = tail.next = new SC._ChainObserver(parts[i], root); 45 } 46 47 var tails = root.tails = [tail]; // ex. [_ChainObserver({ property: 'propC' })] 48 49 // Now root has the first observer and tail has the last one. 50 // Feed the rootObject into the front to setup the chain... 51 // do this BEFORE we set the target/method so they will not be triggered. 52 root.objectDidChange(rootObject); 53 54 tails.forEach(function (tail) { 55 // Finally, set the target/method on the tail so that future changes will trigger. 56 tail.target = target; 57 tail.method = method; 58 tail.context = context; 59 }); 60 61 // no need to hold onto references to the tails; if the underlying 62 // objects go away, let them get garbage collected 63 root.tails = null; 64 65 // and return the root to save 66 return root; 67 }; 68 69 SC._ChainObserver.prototype = { 70 isChainObserver: true, 71 72 // the object this instance is observing 73 object: null, 74 75 // the property on the object this link is observing. 76 property: null, 77 78 // if not null, this is the next link in the chain. Whenever the 79 // current property changes, the next observer will be notified. 80 next: null, 81 82 root: null, 83 84 // if not null, this is the final target observer. 85 target: null, 86 87 // if not null, this is the final target method 88 method: null, 89 90 // an accessor method that traverses the list and finds the tail 91 tail: function () { 92 if (this._tail) { return this._tail; } 93 94 var tail = this; 95 96 while (tail.next) { 97 tail = tail.next; 98 } 99 100 this._tail = tail; 101 return tail; 102 }, 103 104 // invoked when the source object changes. removes observer on old 105 // object, sets up new observer, if needed. 106 objectDidChange: function (newObject) { 107 if (newObject === this.object) return; // nothing to do. 108 109 // if an old object, remove observer on it. 110 if (this.object) { 111 if (this.property === '@each' && this.object._removeContentObserver) { 112 this.object._removeContentObserver(this); 113 } else if (this.object.removeObserver) { 114 this.object.removeObserver(this.property, this, this.propertyDidChange); 115 } 116 } 117 118 // if a new object, add observer on it... 119 this.object = newObject; 120 121 // when [].propName is used, we will want to set up observers on each item 122 // added to the Enumerable, and remove them when the item is removed from 123 // the Enumerable. 124 // 125 // In this case, we invoke addEnumerableObserver, which handles setting up 126 // and tearing down observers as items are added and removed from the 127 // Enumerable. 128 if (this.property === '@each' && this.next) { 129 if (this.object && this.object._addContentObserver) { 130 this.object._addContentObserver(this); 131 } 132 } else { 133 if (this.object && this.object.addObserver) { 134 this.object.addObserver(this.property, this, this.propertyDidChange); 135 } 136 137 // now, notify myself that my property value has probably changed. 138 this.propertyDidChange(); 139 } 140 }, 141 142 // the observer method invoked when the observed property changes. 143 propertyDidChange: function () { 144 // get the new value 145 var object = this.object; 146 var property = this.property; 147 var value = (object && object.get) ? object.get(property) : null; 148 149 // if we have a next object in the chain, notify it that its object 150 // did change... 151 if (this.next) { this.next.objectDidChange(value); } 152 153 // if we have a target/method, call it. 154 var target = this.target, 155 method = this.method, 156 context = this.context; 157 158 if (target && method) { 159 var rev = object ? object.propertyRevision : null; 160 if (context) { 161 method.call(target, object, property, value, context, rev); 162 } else { 163 method.call(target, object, property, value, rev); 164 } 165 } 166 }, 167 168 // teardown the chain... 169 destroyChain: function () { 170 171 // remove observer 172 var obj = this.object; 173 if (obj) { 174 if (this.property === '@each' && this.next && obj._removeContentObserver) { 175 obj._removeContentObserver(this); 176 } 177 178 if (obj.removeObserver) { 179 obj.removeObserver(this.property, this, this.propertyDidChange); 180 } 181 } 182 183 // destroy next item in chain 184 if (this.next) this.next.destroyChain(); 185 186 // and clear left overs... 187 this.next = this.target = this.method = this.object = this.context = null; 188 return null; 189 } 190 191 }; 192