1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 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 sc_require('controllers/controller') ; 9 10 /** @class 11 12 An ObjectController gives you a simple way to manage the editing state of 13 an object. You can use an ObjectController instance as a "proxy" for your 14 model objects. 15 16 Any properties you get or set on the object controller, will be passed 17 through to its content object. This allows you to setup bindings to your 18 object controller one time for all of your views and then swap out the 19 content as needed. 20 21 ## Working with Arrays 22 23 An ObjectController can accept both arrays and single objects as content. 24 If the content is an array, the ObjectController will do its best to treat 25 the array as a single object. For example, if you set the content of an 26 ObjectController to an array of Contact records and then call: 27 28 contactController.get('name'); 29 30 The controller will check the name property of each Contact in the array. 31 If the value of the property for each Contact is the same, that value will 32 be returned. If the any values are different, then an array will be 33 returned with the values from each Contact in them. 34 35 Most SproutCore views can work with both arrays and single content, which 36 means that most of the time, you can simply hook up your views and this will 37 work. 38 39 If you would prefer to make sure that your ObjectController is always 40 working with a single object and you are using bindings, you can always 41 setup your bindings so that they will convert the content to a single object 42 like so: 43 44 contentBinding: SC.Binding.single('MyApp.listController.selection') ; 45 46 This will ensure that your content property is always a single object 47 instead of an array. 48 49 @extends SC.Controller 50 @since SproutCore 1.0 51 */ 52 SC.ObjectController = SC.Controller.extend( 53 /** @scope SC.ObjectController.prototype */ { 54 55 //@if(debug) 56 /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */ 57 58 /* @private */ 59 toString: function () { 60 var content = this.get('content'), 61 ret = sc_super(); 62 63 return content ? "%@:\n ↳ %@".fmt(ret, content) : ret; 64 }, 65 66 /* END DEBUG ONLY PROPERTIES AND METHODS */ 67 //@endif 68 69 // .......................................................... 70 // PROPERTIES 71 // 72 73 /** 74 Set to the object you want this controller to manage. The object should 75 usually be a single value; not an array or enumerable. If you do supply 76 an array or enumerable with a single item in it, the ObjectController 77 will manage that single item. 78 79 Usually your content object should implement the SC.Observable mixin, but 80 this is not required. All SC.Object-based objects support SC.Observable 81 82 @type Object 83 */ 84 content: null, 85 86 /** 87 If YES, then setting the content to an enumerable or an array with more 88 than one item will cause the Controller to attempt to treat the array as 89 a single object. Use of get(), for example, will get every property on 90 the enumerable and return it. set() will set the property on every item 91 in the enumerable. 92 93 If NO, then setting content to an enumerable with multiple items will be 94 treated like setting a null value. hasContent will be NO. 95 96 @type Boolean 97 */ 98 allowsMultipleContent: NO, 99 100 /** 101 Becomes YES whenever this object is managing content. Usually this means 102 the content property contains a single object or an array or enumerable 103 with a single item. Array's or enumerables with multiple items will 104 normally make this property NO unless allowsMultipleContent is YES. 105 106 @type Boolean 107 */ 108 hasContent: function() { 109 return !SC.none(this.get('observableContent')); 110 }.property('observableContent'), 111 112 /** 113 Makes a controller editable or not editable. The SC.Controller class 114 itself does not do anything with this property but subclasses will 115 respect it when modifying content. 116 117 @type Boolean 118 */ 119 isEditable: YES, 120 121 /** 122 Primarily for internal use. Normally you should not access this property 123 directly. 124 125 Returns the actual observable object proxied by this controller. Usually 126 this property will mirror the content property. In some cases - notably 127 when setting content to an enumerable, this may return a different object. 128 129 Note that if you set the content to an enumerable which itself contains 130 enumerables and allowsMultipleContent is NO, this will become null. 131 132 @type Object 133 */ 134 observableContent: function() { 135 var content = this.get('content'), 136 len, allowsMultiple; 137 138 // if enumerable, extract the first item or possibly become null 139 if (content && content.isEnumerable) { 140 len = content.get('length'); 141 allowsMultiple = this.get('allowsMultipleContent'); 142 143 if (len === 1) content = content.firstObject(); 144 else if (len===0 || !allowsMultiple) content = null; 145 146 // if we got some new content, it better not be enum also... 147 if (content && !allowsMultiple && content.isEnumerable) content=null; 148 } 149 150 return content; 151 }.property('content', 'allowsMultipleContent').cacheable(), 152 153 // .......................................................... 154 // METHODS 155 // 156 157 /** 158 Override this method to destroy the selected object. 159 160 The default just passes this call onto the content object if it supports 161 it, and then sets the content to null. 162 163 Unlike most calls to destroy() this will not actually destroy the 164 controller itself; only the the content. You continue to use the 165 controller by setting the content to a new value. 166 167 @returns {SC.ObjectController} receiver 168 */ 169 destroy: function() { 170 var content = this.get('observableContent') ; 171 if (content && SC.typeOf(content.destroy) === SC.T_FUNCTION) { 172 content.destroy(); 173 } 174 this.set('content', null) ; 175 return this; 176 }, 177 178 /** 179 Invoked whenever any property on the content object changes. 180 181 The default implementation will simply notify any observers that the 182 property has changed. You can override this method if you need to do 183 some custom work when the content property changes. 184 185 If you have set the content property to an enumerable with multiple 186 objects and you set allowsMultipleContent to YES, this method will be 187 called anytime any property in the set changes. 188 189 If all properties have changed on the content or if the content itself 190 has changed, this method will be called with a key of "*". 191 192 @param {Object} target the content object 193 @param {String} key the property that changes 194 @returns {void} 195 */ 196 contentPropertyDidChange: function(target, key) { 197 if (key === '*') this.allPropertiesDidChange(); 198 else this.notifyPropertyChange(key); 199 }, 200 201 /** 202 Called whenver you try to get/set an unknown property. The default 203 implementation will pass through to the underlying content object but 204 you can override this method to do some other kind of processing if 205 needed. 206 207 @param {String} key key being retrieved 208 @param {Object} value value to set or undefined if reading only 209 @returns {Object} property value 210 */ 211 unknownProperty: function(key,value) { 212 213 // avoid circular references 214 if (key==='content') { 215 if (value !== undefined) this.content = value; 216 return this.content; 217 } 218 219 // for all other keys, just pass through to the observable object if 220 // there is one. Use getEach() and setEach() on enumerable objects. 221 var content = this.get('observableContent'), loc, cur, isSame; 222 if (content===null || content===undefined) return undefined; // empty 223 224 // getter... 225 if (value === undefined) { 226 if (content.isEnumerable) { 227 value = content.getEach(key); 228 229 // iterate over array to see if all values are the same. if so, then 230 // just return that value 231 loc = value.get('length'); 232 if (loc>0) { 233 isSame = YES; 234 cur = value.objectAt(0); 235 while((--loc > 0) && isSame) { 236 if (cur !== value.objectAt(loc)) isSame = NO ; 237 } 238 if (isSame) value = cur; 239 } else value = undefined; // empty array. 240 241 } else value = (content.isObservable) ? content.get(key) : content[key]; 242 243 // setter 244 } else { 245 if (!this.get('isEditable')) { 246 throw new Error("%@.%@ is not editable".fmt(this,key)); 247 } 248 249 if (content.isEnumerable) content.setEach(key, value); 250 else if (content.isObservable) content.set(key, value); 251 else content[key] = value; 252 } 253 254 return value; 255 }, 256 257 // ............................... 258 // INTERNAL SUPPORT 259 // 260 261 /** @private - setup observer on init if needed. */ 262 init: function() { 263 sc_super(); 264 if (this.get('content')) this._scoc_contentDidChange(); 265 if (this.get('observableContent')) this._scoc_observableContentDidChange(); 266 }, 267 268 _scoc_contentDidChange: function () { 269 var last = this._scoc_content, 270 cur = this.get('content'); 271 272 if (last !== cur) { 273 this._scoc_content = cur; 274 var func = this._scoc_enumerableContentDidChange; 275 if (last && last.isEnumerable) { 276 last.removeObserver('[]', this, func); 277 } 278 if (cur && cur.isEnumerable) { 279 cur.addObserver('[]', this, func); 280 } 281 } 282 }.observes("content"), 283 284 /** @private 285 286 Called whenever the observable content property changes. This will setup 287 observers on the content if needed. 288 */ 289 _scoc_observableContentDidChange: function() { 290 var last = this._scoc_observableContent, 291 cur = this.get('observableContent'), 292 func = this.contentPropertyDidChange, 293 efunc= this._scoc_enumerableContentDidChange; 294 295 if (last === cur) return this; // nothing to do 296 //console.log('observableContentDidChange'); 297 298 this._scoc_observableContent = cur; // save old content 299 300 // stop observing last item -- if enumerable stop observing set 301 if (last) { 302 if (last.isEnumerable) last.removeObserver('[]', this, efunc); 303 else if (last.isObservable) last.removeObserver('*', this, func); 304 } 305 306 if (cur) { 307 if (cur.isEnumerable) cur.addObserver('[]', this, efunc); 308 else if (cur.isObservable) cur.addObserver('*', this, func); 309 } 310 311 // notify! 312 if ((last && last.isEnumerable) || (cur && cur.isEnumerable)) { 313 this._scoc_enumerableContentDidChange(); 314 } else { 315 this.contentPropertyDidChange(cur, '*'); 316 } 317 318 }.observes("observableContent"), 319 320 /** @private 321 Called when observed enumerable content has changed. This will teardown 322 and setup observers on the enumerable content items and then calls 323 contentPropertyDidChange(). This method may be called even if the new 324 'cur' is not enumerable but the last content was enumerable. 325 */ 326 _scoc_enumerableContentDidChange: function() { 327 var cur = this.get('observableContent'), 328 set = this._scoc_observableContentItems, 329 func = this.contentPropertyDidChange; 330 331 // stop observing each old item 332 if (set) { 333 set.forEach(function(item) { 334 if (item.isObservable) item.removeObserver('*', this, func); 335 }, this); 336 set.clear(); 337 } 338 339 // start observing new items if needed 340 if (cur && cur.isEnumerable) { 341 if (!set) set = SC.Set.create(); 342 cur.forEach(function(item) { 343 if (set.contains(item)) return ; // nothing to do 344 set.add(item); 345 if (item.isObservable) item.addObserver('*', this, func); 346 }, this); 347 } else set = null; 348 349 this._scoc_observableContentItems = set; // save for later cleanup 350 351 // notify 352 this.contentPropertyDidChange(cur, '*'); 353 return this ; 354 } 355 356 }) ; 357