1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2010 Sprout Systems, 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 @namespace 10 11 This mixin allows a view to get its value from a content object based 12 on the value of its contentValueKey. 13 14 myView = SC.View.create({ 15 content: {prop: "abc123"}, 16 17 contentValueKey: 'prop' 18 }); 19 20 // myView.get('value') will be "abc123" 21 22 This is useful if you have a nested record structure and want to have 23 it be reflected in a nested view structure. If your data structures 24 only have primitive values, consider using SC.Control instead. 25 */ 26 SC.ContentValueSupport = { 27 /** 28 Walk like a duck. 29 30 @type Boolean 31 @default YES 32 */ 33 hasContentValueSupport: YES, 34 35 /** @private */ 36 initMixin: function () { 37 // setup content observing if needed. 38 this._control_contentKeysDidChange(); 39 }, 40 41 /** @private */ 42 destroyMixin: function () { 43 // Remove old observers on self. 44 this._cleanup_old_observers(); 45 46 // Remove old observers on content. 47 this._cleanup_old_content_observers(); 48 }, 49 50 /** 51 The value represented by this control. 52 53 Most controls represent a value of some type, such as a number, string 54 or image URL. This property should hold that value. It is bindable 55 and observable. Changing this value will immediately change the 56 appearance of the control. Likewise, editing the control 57 will immediately change this value. 58 59 If instead of setting a single value on a control, you would like to 60 set a content object and have the control display a single property 61 of that control, then you should use the content property instead. 62 63 @type Object 64 @default null 65 */ 66 value: null, 67 68 /** 69 The content object represented by this control. 70 71 Often you need to use a control to display some single aspect of an 72 object, especially if you are using the control as an item view in a 73 collection view. 74 75 In those cases, you can set the content and contentValueKey for the 76 control. This will cause the control to observe the content object for 77 changes to the value property and then set the value of that property 78 on the "value" property of this object. 79 80 Note that unless you are using this control as part of a form or 81 collection view, then it would be better to instead bind the value of 82 the control directly to a controller property. 83 84 @type SC.Object 85 @default null 86 */ 87 content: null, 88 89 /** 90 Keys that should be observed on the content object and mapped to values on 91 this object. Should be a hash of local keys that point to keys on the content to 92 map to local values. For example, the default is {'contentValueKey': 'value'}. 93 This means that the value of this.contentValueKey will be observed as a key on 94 the content object and its value will be mapped to this.value. 95 96 @type Hash 97 @default null 98 */ 99 contentKeys: null, 100 101 _default_contentKeys: { 102 contentValueKey: 'value' 103 }, 104 105 /** 106 The property on the content object that would want to represent the 107 value of this control. This property should only be set before the 108 content object is first set. If you have a displayDelegate, then 109 you can also use the contentValueKey of the displayDelegate. 110 111 @type String 112 @default null 113 */ 114 contentValueKey: null, 115 116 /** 117 Invoked whenever any property on the content object changes. 118 119 The default implementation will update the value property of the view 120 if the contentValueKey property has changed. You can override this 121 method to implement whatever additional changes you would like. 122 123 The key will typically contain the name of the property that changed or 124 '*' if the content object itself has changed. You should generally do 125 a total reset if '*' is changed. 126 127 @param {Object} target the content object 128 @param {String} key the property that changes 129 @returns {void} 130 @test in content 131 */ 132 contentPropertyDidChange: function (target, key) { 133 var contentKeys = this.get('contentKeys'); 134 135 if (contentKeys) { 136 var contentKey; 137 138 for (contentKey in contentKeys) { 139 // if we found the specific contentKey, then just update that and we're done 140 if (key === this.getDelegateProperty(contentKey, this, this.get('displayDelegate'), contentKeys)) { 141 return this.updatePropertyFromContent(contentKeys[contentKey], key, contentKey, target); 142 } 143 144 // else if '*' is changed, then update for every contentKey 145 else if (key === '*') { 146 this.updatePropertyFromContent(contentKeys[contentKey], key, contentKey, target); 147 } 148 } 149 } 150 151 else { 152 return this.updatePropertyFromContent('value', key, 'contentValueKey', target); 153 } 154 }, 155 156 /** 157 Helper method you can use from your own implementation of 158 contentPropertyDidChange(). This method will look up the content key to 159 extract a property and then update the property if needed. If you do 160 not pass the content key or the content object, they will be computed 161 for you. It is more efficient, however, for you to compute these values 162 yourself if you expect this method to be called frequently. 163 164 @param {String} prop local property to update 165 @param {String} key the contentproperty that changed 166 @param {String} contentKey the local property that contains the key 167 @param {Object} content 168 @returns {SC.Control} receiver 169 */ 170 updatePropertyFromContent: function (prop, key, contentKey, content) { 171 var del, v; 172 173 if (contentKey === undefined) contentKey = "content" + prop.capitalize() + "Key"; 174 175 // prefer our own definition of contentKey 176 if (this[contentKey]) contentKey = this.get(contentKey); 177 // if we don't have one defined check the delegate 178 else if ((del = this.get('displayDelegate')) && (v = del[contentKey])) contentKey = del.get ? del.get(contentKey) : v; 179 // if we have no key we can't do anything so just short circuit out 180 else return this; 181 182 // only bother setting value if the observer triggered for the correct key 183 if (key === '*' || key === contentKey) { 184 if (content === undefined) content = this.get('content'); 185 186 if (content) v = content.get ? content.get(contentKey) : content[contentKey]; 187 else v = null; 188 189 this.setIfChanged(prop, v); 190 } 191 192 return this; 193 }, 194 195 /** 196 Relays changes to the value back to the content object if you are using 197 a content object. 198 199 This observer is triggered whenever the value changes. It will only do 200 something if it finds you are using the content property and 201 contentValueKey and the new value does not match the old value of the 202 content object. 203 204 If you are using contentValueKey in some other way than typically 205 implemented by this mixin, then you may want to override this method as 206 well. 207 208 @returns {void} 209 */ 210 updateContentWithValueObserver: function (target, key) { 211 var reverseContentKeys = this._reverseContentKeys; 212 213 // if everything changed, iterate through and update them all 214 if (!key || key === '*') { 215 for (key in reverseContentKeys) { 216 this.updateContentWithValueObserver(this, key); 217 } 218 } 219 220 // get value -- set on content if changed 221 var value = this.get(key); 222 223 var content = this.get('content'), 224 // get the key we should be setting on content, asking displayDelegate if 225 // necessary 226 contentKey = this.getDelegateProperty(reverseContentKeys[key], this, this.displayDelegate); 227 228 // do nothing if disabled 229 if (!contentKey || !content) return this; 230 231 if (typeof content.setIfChanged === SC.T_FUNCTION) { 232 content.setIfChanged(contentKey, value); 233 } 234 235 // avoid re-writing inherited props 236 else if (content[contentKey] !== value) { 237 content[contentKey] = value; 238 } 239 }, 240 241 /** @private 242 This should be null so that if content is also null, the 243 _contentDidChange won't do anything on init. 244 */ 245 _control_content: null, 246 _old_contentValueKeys: null, 247 _old_contentKeys: null, 248 249 /** @private 250 Observes when a content object has changed and handles notifying 251 changes to the value of the content object. 252 253 Optimized for the default case of only observing contentValueKey. If you use 254 a custom value for contentKeys it will switch to using a CoreSet to track 255 observed keys. 256 */ 257 _control_contentDidChange: function (target, key) { 258 // remove an observer from the old content if necessary 259 this._cleanup_old_content_observers(); 260 261 var content = this.get('content'), 262 contentKeys = this.get('contentKeys'), contentKey, 263 oldKeys = this._old_contentValueKeys, 264 f = this.contentPropertyDidChange; 265 266 // add observer to new content if necessary. 267 if (content && content.addObserver) { 268 // set case 269 if (contentKeys) { 270 // lazily create the key set 271 if (!oldKeys) oldKeys = SC.CoreSet.create(); 272 273 // add observers to each key 274 for (contentKey in contentKeys) { 275 contentKey = this.getDelegateProperty(contentKey, this, this.get('displayDelegate')); 276 277 if (contentKey) { 278 content.addObserver(contentKey, this, f); 279 280 oldKeys.add(contentKey); 281 } 282 } 283 } 284 285 // default case hardcoded for contentValueKey 286 else { 287 contentKey = this.getDelegateProperty('contentValueKey', this, this.get('displayDelegate')); 288 289 if (contentKey) { 290 content.addObserver(contentKey, this, f); 291 292 // if we had a set before, continue using it 293 if (oldKeys) oldKeys.add(contentKey); 294 // otherwise just use a string 295 else oldKeys = contentKey; 296 } 297 } 298 } 299 300 // notify that values did change. 301 key = (!key || key === 'content') ? '*' : this.get(key); 302 if (key) this.contentPropertyDidChange(content, key); 303 304 // Cache values for clean up. 305 this._control_content = content; 306 this._old_contentValueKeys = oldKeys; 307 }.observes('content'), 308 309 /** @private 310 Observes changes to contentKeys and sets up observers on the local keys to 311 update the observers on the content object. 312 */ 313 _control_contentKeysDidChange: function () { 314 var key, reverse = {}, 315 // if no hash is present, use the default contentValueKey -> value 316 contentKeys = this.get('contentKeys') || this._default_contentKeys, 317 contentKey, 318 f = this._control_contentDidChange, 319 reverseF = this.updateContentWithValueObserver; 320 321 // Remove old observers. 322 this._cleanup_old_observers(); 323 324 // add new observers 325 for (key in contentKeys) { 326 contentKey = contentKeys[key]; 327 328 // build reverse mapping to update content with value 329 reverse[contentKey] = key; 330 331 // add value observer 332 this.addObserver(contentKey, this, reverseF); 333 334 // add content key observer 335 this.addObserver(key, this, f); 336 } 337 338 // store reverse map for later use 339 this._reverseContentKeys = reverse; 340 341 this._old_contentKeys = contentKeys; 342 343 // call the other observer now to update all the observers 344 this._control_contentDidChange(); 345 }.observes('contentKeys'), 346 347 /** @private */ 348 _cleanup_old_content_observers: function () { 349 var old = this._control_content, 350 oldKeys = this._old_contentValueKeys, 351 oldType = SC.typeOf(oldKeys), 352 f = this.contentPropertyDidChange, 353 contentKey; 354 355 if (old && old.removeObserver && oldKeys) { 356 // default case 357 if (oldType === SC.T_STRING) { 358 old.removeObserver(oldKeys, this, f); 359 360 this._old_contentValueKeys = oldKeys = null; 361 } 362 363 // set case 364 else { 365 var i, len = oldKeys.get('length'); 366 367 for (i = 0; i < len; i++) { 368 contentKey = oldKeys[i]; 369 370 old.removeObserver(contentKey, this, f); 371 } 372 373 oldKeys.clear(); 374 } 375 } 376 }, 377 378 /** @private */ 379 _cleanup_old_observers: function () { 380 var oldContentKeys = this._old_contentKeys, 381 f = this._control_contentDidChange, 382 reverseF = this.updateContentWithValueObserver, 383 contentKey, key; 384 385 // remove old observers 386 for (key in oldContentKeys) { 387 contentKey = oldContentKeys[key]; 388 389 this.removeObserver(contentKey, this, reverseF); 390 this.removeObserver(key, this, f); 391 } 392 } 393 }; 394 395