1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2010 Evin Grano 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 /** 9 @class 10 11 A `ChildArray` is used to map an array of `ChildRecord` objects. 12 13 @extends SC.Enumerable 14 @extends SC.Array 15 @since SproutCore 1.0 16 */ 17 18 SC.ChildArray = SC.Object.extend(SC.Enumerable, SC.Array, 19 /** @scope SC.ChildArray.prototype */ { 20 21 //@if(debug) 22 /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */ 23 24 /* @private */ 25 toString: function () { 26 var propertyName = this.get('propertyName'), 27 length = this.get('length'); 28 29 return "%@({ propertyName: '%@', length: %@, … })".fmt(sc_super(), propertyName, length); 30 }, 31 32 /* END DEBUG ONLY PROPERTIES AND METHODS */ 33 //@endif 34 35 /** 36 If set, it is the default record `recordType` 37 38 @default null 39 @type String 40 */ 41 defaultRecordType: null, 42 43 /** 44 If set, the parent record will be notified whenever the array changes so that 45 it can change its own state 46 47 @default null 48 @type {SC.Record} 49 */ 50 record: null, 51 52 /** 53 The name of the attribute in the parent record's datahash that represents 54 this child array's data. 55 56 @default null 57 @type String 58 */ 59 propertyName: null, 60 61 /** 62 The store that owns this child array's parent record. 63 64 @type SC.Store 65 @readonly 66 */ 67 store: function() { 68 return this.getPath('record.store'); 69 }.property('record').cacheable(), 70 71 /** 72 The storeKey for the parent record of this child array. 73 74 @type Number 75 @readonly 76 */ 77 storeKey: function() { 78 return this.getPath('record.storeKey'); 79 }.property('record').cacheable(), 80 81 /** 82 Returns the original child array of JavaScript Objects. 83 84 Note: Avoid modifying this array directly, because changes will not be 85 reflected by this SC.ChildArray. 86 87 @type SC.Array 88 @property 89 */ 90 readOnlyChildren: function() { 91 return this.get('record').readAttribute(this.get('propertyName')); 92 }.property(), 93 94 /** 95 Returns an editable child array of JavaScript Objects. 96 97 Any changes to this array will not affect the parent record's datahash. 98 99 @type {SC.Array} 100 @property 101 */ 102 editableChildren: function() { 103 var store = this.get('store'), 104 storeKey = this.get('storeKey'), 105 pname = this.get('propertyName'), 106 ret, hash; 107 108 ret = store.readEditableProperty(storeKey, pname); 109 if (!ret) { 110 hash = store.readEditableDataHash(storeKey); 111 ret = hash[pname] = []; 112 } 113 114 return ret ; 115 }.property(), 116 117 // .......................................................... 118 // ARRAY PRIMITIVES 119 // 120 121 /** @private 122 Returned length is a pass-through to the storeIds array. 123 124 @type Number 125 @property 126 */ 127 length: function() { 128 var children = this.get('readOnlyChildren'); 129 return children ? children.length : 0; 130 }.property('readOnlyChildren'), 131 132 /** 133 Looks up the store id in the store ids array and materializes a 134 records. 135 136 @param {Number} idx index of the object to retrieve. 137 @returns {SC.Record} The nested record if found or undefined if not. 138 */ 139 objectAt: function(idx) { 140 var recs = this._records, 141 children = this.get('readOnlyChildren'), 142 hash, ret, 143 pname = this.get('propertyName'), 144 parent = this.get('record'), 145 len = children ? children.length : 0; 146 147 if (!children) return undefined; // nothing to do 148 if (recs && (ret=recs[idx])) return ret ; // cached 149 150 // If not a good index return undefined 151 if (idx >= len) return undefined; 152 hash = children.objectAt(idx); 153 if (!hash) return undefined; 154 155 // not in cache, materialize 156 if (!recs) this._records = recs = []; // create cache 157 recs[idx] = ret = parent.registerNestedRecord(hash, pname, pname+'.'+idx); 158 159 return ret; 160 }, 161 162 /** 163 Pass through to the underlying array. The passed in objects should be 164 nested SC.Records, which can be converted to JavaScript objects or 165 JavaScript objects themselves. 166 167 @param {Number} idx index of the object to replace. 168 @param {Number} amt number of objects to replace starting at idx. 169 @param {Number} recs array with records to replace. These may be JavaScript objects or nested SC.Record objects. 170 @returns {SC.ChildArray} 171 */ 172 replace: function(idx, amt, recs) { 173 var children = this.get('editableChildren'), 174 recsLen = recs ? (recs.get ? recs.get('length') : recs.length) : 0, 175 parent = this.get('record'), 176 pname = this.get('propertyName'), 177 store = this.get('store'), 178 removeCount, addCount, 179 defaultRecordType, storeKeysById, 180 newObjects, rec, 181 i, len; 182 183 // Create the proxy cache, we will need it. 184 if (!this._records) this._records = []; 185 186 // Convert any SC.Record objects into JavaScript objects. 187 newObjects = this._processRecordsToHashes(recs); 188 189 // Unregister the records being replaced. 190 // for (i = idx, len = children.length; i < len; ++i) { 191 // this.unregisterNestedRecord(i); 192 // } 193 194 // Ensure that all removed objects are pre-registered in case any instances are outstanding. 195 // These objects will improperly reflect being registered to this parent, but 196 // at least they won't conflict with the actual associated records once we 197 // disassociate them from the record type. 198 defaultRecordType = this.get('defaultRecordType'); 199 storeKeysById = defaultRecordType.storeKeysById(); 200 201 for (i = idx, len = idx + amt; i < len; i++) { 202 rec = this._records[i]; 203 204 if (!rec) { 205 rec = parent.registerNestedRecord(children[i], pname, pname + '.' + i); 206 } else { 207 // Remove the cached record. 208 this._records[i] = null; 209 } 210 211 // Now throw away the connection, so that the parent won't retrieve this 212 // same instance. This is a work-around due to the fact that nested records 213 // are proxied through their parent records. 214 storeKeysById[rec.get('id')] = null; 215 } 216 217 // All materialized nested records after idx + amt to end need to be removed 218 // because the paths will no longer be valid. 219 for (i = idx + amt, len = this._records.length; i < len; i++) { 220 rec = this._records[i]; 221 222 if (rec) { 223 store.unregisterChildFromParent(rec.get('storeKey')); 224 225 this._records[i] = null; 226 } 227 } 228 229 // All objects from idx to the end must be removed to do an insert. 230 removeCount = children.length - idx; 231 addCount = children.length - idx - amt + recsLen; 232 233 this.arrayContentWillChange(idx, removeCount, addCount); 234 235 // Perform a raw array replace without any KVO checks. 236 if (newObjects.length === 0) { 237 children.splice(idx, amt); 238 } else { 239 var args = [idx, amt].concat(newObjects); 240 children.splice.apply(children, args); 241 } 242 243 // All current SC.Record instances must be updated to their new backing object. 244 // For example, when passing an SC.Record object in, that instance should 245 // update to reflect its new nested object path. 246 for (i = idx, len = children.length; i < len; i++) { 247 this._records[i] = parent.registerNestedRecord(children[i], pname, pname + '.' + i); 248 } 249 250 // Update the enumerable, [], property (including firstObject and lastObject) 251 this.arrayContentDidChange(idx, removeCount, addCount); 252 253 // Update our cache! So when the record property change comes back down we can ignore it. 254 this._sc_prevChildren = children; 255 256 // We must indicate to the parent that we have been modified, so they can 257 // update their status. 258 parent.recordDidChange(pname); 259 260 return this; 261 }, 262 263 /** 264 Unregisters a child record from its parent record. 265 266 Since accessing a child (nested) record creates a new data hash for the 267 child and caches the child record and its relationship to the parent record, 268 it's important to clear those caches when the child record is overwritten 269 or removed. This function tells the store to remove the child record from 270 the store's various child record caches. 271 272 You should not need to call this function directly. Simply setting the 273 child record property on the parent to a different value will cause the 274 previous child record to be unregistered. 275 276 @param {Number} idx The index of the child record. 277 */ 278 // unregisterNestedRecord: function(idx) { 279 // var childArray, childRecord, csk, store, 280 // record = this.get('record'), 281 // pname = this.get('propertyName'); 282 // 283 // store = record.get('store'); 284 // childArray = record.getPath(pname); 285 // childRecord = childArray.objectAt(idx); 286 // csk = childRecord.get('storeKey'); 287 // store.unregisterChildFromParent(csk); 288 // }, 289 290 /** 291 Calls normalize on each object in the array 292 */ 293 normalize: function(){ 294 this.forEach(function (rec) { 295 if (rec.normalize) rec.normalize(); 296 }); 297 }, 298 299 // .......................................................... 300 // INTERNAL SUPPORT 301 // 302 303 /** @private Converts any SC.Records in the array into an array of hashes. 304 305 @param {SC.Array} recs records to be converted to hashes. 306 @returns {SC.Array} array of hashes. 307 */ 308 _processRecordsToHashes: function (recs) { 309 var store, sk, 310 ret = []; 311 312 recs.forEach(function (rec, idx) { 313 if (rec.isNestedRecord) { 314 store = rec.get('store'); 315 sk = rec.storeKey; 316 ret[idx] = store.readDataHash(sk); 317 } else { 318 ret[idx] = rec; 319 } 320 }); 321 322 return ret; 323 }, 324 325 /** @private 326 This is called by the parent record whenever its properties change. It is 327 also called by the ChildrenAttribute transform when the attribute is set 328 to a new array. 329 */ 330 recordPropertyDidChange: function (keys) { 331 var oldLength = this.get('length'), 332 children = this.get('readOnlyChildren'), 333 newLength = children ? children.length : 0, 334 // store = this.get('store'), 335 prevChildren = this._sc_prevChildren; 336 337 // Fast Path! No actual change to our backing array attribute so we should 338 // not notify any changes. 339 if (prevChildren === children) { return; } 340 341 // TODO: We can not use this, because removed instances will lose their 342 // connection to their data hashes in the store. There is an ugly hack in 343 // SC.Store#writeDataHash which can't handle this. 344 // All materialized nested records need to be removed. They are no longer valid! 345 // if (this._records) { 346 // for (var i = 0, len = this._records.length; i < len; i++) { 347 // var rec = this._records[i]; 348 349 // // Unregister the nested record. 350 // if (rec) { 351 // store.unregisterChildFromParent(rec.get('storeKey')); 352 // } 353 // } 354 355 // // Throw away our cache. 356 // this._records.length = 0; 357 // } 358 359 // Throw away our cache. 360 this._records = null; 361 362 // this.arrayContentWillChange(0, oldLength, newLength); 363 this.arrayContentDidChange(0, oldLength, newLength); 364 365 // Cache our backing array so we can avoid updates when we haven't actually 366 // changed. See fast path above. 367 this._sc_prevChildren = children; 368 } 369 370 }) ; 371