1 sc_require('views/template'); 2 3 /** @class 4 5 @author Tom Dale 6 @author Yehuda Katz 7 @extends SC.TemplateView 8 @since SproutCore 1.5 9 */ 10 SC.TemplateCollectionView = SC.TemplateView.extend( 11 /** @scope SC.TemplateCollectionView.prototype */{ 12 /** 13 Name of the tag that is used for the collection 14 15 If the tag is a list ('ul' or 'ol') each item will be wrapped into a 'li' tag. 16 If the tag is a table ('table', 'thead', 'tbody') each item will be wrapped into a 'tr' tag. 17 18 @type String 19 @default ul 20 */ 21 tagName: 'ul', 22 23 /** 24 A list of items to be displayed by the TemplateCollectionView. 25 26 @type SC.Array 27 @default null 28 */ 29 content: null, 30 31 template: SC.Handlebars.compile(''), 32 33 /** 34 An optional view to display if content is set to an empty array. 35 36 @type SC.TemplateView 37 @default null 38 */ 39 emptyView: null, 40 41 /** 42 @private 43 When the view is destroyed, remove array observers on the content array. 44 */ 45 destroy: function() { 46 var content = this.get('content'); 47 if(content) { 48 content.removeArrayObservers({ 49 target: this, 50 willChange: 'arrayContentWillChange', 51 didChange: 'arrayContentDidChange' 52 }); 53 } 54 this.removeObserver('content', this, this._sctcv_contentDidChange); 55 return sc_super(); 56 }, 57 58 // In case a default content was set, trigger the child view creation 59 // as soon as the empty layer was created 60 didCreateLayer: function() { 61 // FIXME: didCreateLayer gets called multiple times when template collection 62 // views are nested - this is a hack to avoid rendering the content more 63 // than once. 64 if (this._sctcv_layerCreated) { return; } 65 this._sctcv_layerCreated = true; 66 67 //set up array observers on the content array. 68 this.addObserver('content', this, this._sctcv_contentDidChange); 69 this._sctcv_contentDidChange(); 70 }, 71 72 /** 73 @type SC.TemplateView 74 @default SC.TemplateView 75 */ 76 itemView: 'SC.TemplateView', 77 78 /** 79 The template used to render each item in the collection. 80 81 This should be a function that takes a content object and returns 82 a string of HTML that will be inserted into the DOM. 83 84 In general, you should set the `itemViewTemplateName` property instead of 85 setting the `itemViewTemplate` property yourself. If you created the 86 SC.TemplateCollectionView using the Handlebars {{#collection}} helper, this 87 will be set for you automatically. 88 89 @type Function 90 */ 91 itemViewTemplate: null, 92 93 /** 94 The name of the template to lookup if no item view template is provided. 95 96 The collection will look for a template with this name in the global 97 `SC.TEMPLATES` hash. Usually this hash will be populated for you 98 automatically when you include `.handlebars` files in your project. 99 100 @type String 101 */ 102 itemViewTemplateName: null, 103 104 /** 105 A template to render when there is no content or the content length is 0. 106 */ 107 inverseTemplate: function(key, value) { 108 if (value !== undefined) { 109 return value; 110 } 111 112 var templateName = this.get('inverseTemplateName'), 113 template = this.get('templates').get(templateName); 114 115 if (!template) { 116 //@if(debug) 117 if (templateName) { 118 SC.Logger.warn('%@ - Unable to find template "%@".'.fmt(this, templateName)); 119 } 120 //@endif 121 122 return function() { return ''; }; 123 } 124 125 return template; 126 }.property('inverseTemplateName').cacheable(), 127 128 /** 129 The name of a template to lookup if no inverse template is provided. 130 131 @type String 132 */ 133 inverseTemplateName: null, 134 135 itemContext: null, 136 137 itemViewClass: function() { 138 var itemView = this.get('itemView'); 139 var itemViewTemplate = this.get('itemViewTemplate'); 140 var itemViewTemplateName = this.get('itemViewTemplateName'); 141 142 // hash of properties to override in our 143 // item view class 144 var extensions = {}; 145 146 if(SC.typeOf(itemView) === SC.T_STRING) { 147 itemView = SC.objectForPropertyPath(itemView); 148 } 149 150 if (!itemViewTemplate && itemViewTemplateName) { 151 itemViewTemplate = this.get('templates').get(itemViewTemplateName); 152 } 153 154 if (itemViewTemplate) { 155 extensions.template = itemViewTemplate; 156 } 157 158 // If the itemView has not defined a unique tagName, then check for a unique item tagName 159 // to match the given collection tagName. This is safe, since the unique item tagNames 160 // are required by HTML to be children of the special collection tagName. If the collection 161 // doesn't have a special tagName, then the default value of SC.TemplateView is still 162 // used. 163 if (itemView.prototype.tagName === SC.TemplateView.prototype.tagName) { 164 extensions.tagName = this.get('itemTagName'); 165 } 166 167 return itemView.extend(extensions); 168 }.property('itemView').cacheable(), 169 170 /** 171 @private 172 173 When the content property of the collection changes, remove any existing 174 child views and observers, then set up an observer on the new content, if 175 needed. 176 */ 177 _sctcv_contentDidChange: function() { 178 179 var oldContent = this._content, oldLen = 0; 180 var content = this.get('content'), newLen = 0; 181 182 if (oldContent) { 183 oldContent.removeArrayObservers({ 184 target: this, 185 willChange: 'arrayContentWillChange', 186 didChange: 'arrayContentDidChange' 187 }); 188 189 oldLen = oldContent.get('length'); 190 } 191 192 if (content) { 193 content.addArrayObservers({ 194 target: this, 195 willChange: 'arrayContentWillChange', 196 didChange: 'arrayContentDidChange' 197 }); 198 199 newLen = content.get('length'); 200 } 201 202 this.arrayContentWillChange(0, oldLen, newLen); 203 this._content = this.get('content'); 204 this.arrayContentDidChange(0, oldLen, newLen); 205 }, 206 207 arrayContentWillChange: function(start, removedCount, addedCount) { 208 // If the contents were empty before and this template collection has an empty view 209 // remove it now. 210 var emptyView = this.get('emptyView'); 211 if (emptyView) { emptyView.destroy(); } 212 213 // Loop through child views that correspond with the removed items. 214 // Note that we loop from the end of the array to the beginning because 215 // we are mutating it as we go. 216 var childViews = this.get('childViews'), childView, idx, len; 217 218 len = childViews.get('length'); 219 for (idx = start+removedCount-1; idx >= start; idx--) { 220 childView = childViews[idx]; 221 if(childView) { 222 childView.destroy(); 223 } 224 } 225 }, 226 227 /** 228 Called when a mutation to the underlying content array occurs. 229 230 This method will replay that mutation against the views that compose the 231 SC.TemplateCollectionView, ensuring that the view reflects the model. 232 233 This enumerable observer is added in contentDidChange. 234 235 @param {Array} addedObjects the objects that were added to the content 236 @param {Array} removedObjects the objects that were removed from the content 237 @param {Number} changeIndex the index at which the changes occurred 238 */ 239 arrayContentDidChange: function(start, removedCount, addedCount) { 240 if (!this.get('layer')) { return; } 241 242 var content = this.get('content'), 243 itemViewClass = this.get('itemViewClass'), 244 childViews = this.get('childViews'), 245 addedViews = [], 246 renderFunc, childView, itemOptions, elem = this.$(), insertAtElement, item, itemElem, idx, len; 247 248 if (content) { 249 var addedObjects = content.slice(start, start+addedCount); 250 251 // If we have content to display, create a view for 252 // each item. 253 itemOptions = this.get('itemViewOptions') || {}; 254 255 insertAtElement = elem.find('li')[start-1] || null; 256 len = addedObjects.get('length'); 257 258 // TODO: This logic is duplicated from the view helper. Refactor 259 // it so we can share logic. 260 var itemAttrs = { 261 "id": itemOptions.id, 262 "class": itemOptions['class'], 263 "classBinding": itemOptions.classBinding 264 }; 265 266 renderFunc = function(context) { 267 sc_super(); 268 SC.Handlebars.ViewHelper.applyAttributes(itemAttrs, this, context); 269 }; 270 271 itemOptions = SC.clone(itemOptions); 272 delete itemOptions.id; 273 delete itemOptions['class']; 274 delete itemOptions.classBinding; 275 276 for (idx = 0; idx < len; idx++) { 277 item = addedObjects.objectAt(idx); 278 childView = this.createChildView(itemViewClass.extend(itemOptions, { 279 content: item, 280 render: renderFunc, 281 // Use the itemTagName property if it is set, over the tagName of the itemViewClass which is 'div' by default 282 tagName: itemOptions.tagName || itemViewClass.prototype.tagName 283 })); 284 285 var contextProperty = childView.get('contextProperty'); 286 if (contextProperty) { 287 childView.set('context', childView.get(contextProperty)); 288 } 289 290 itemElem = childView.createLayer().$(); 291 if (!insertAtElement) { 292 elem.append(itemElem); 293 } else { 294 itemElem.insertAfter(insertAtElement); 295 } 296 insertAtElement = itemElem; 297 298 addedViews.push(childView); 299 } 300 301 childViews.replace(start, 0, addedViews); 302 } 303 304 var inverseTemplate = this.get('inverseTemplate'); 305 if (childViews.get('length') === 0 && inverseTemplate) { 306 childView = this.createChildView(SC.TemplateView.extend({ 307 template: inverseTemplate, 308 content: this 309 })); 310 this.set('emptyView', childView); 311 childView.createLayer().$().appendTo(elem); 312 this.childViews = [childView]; 313 } 314 315 // Because the layer has been modified, we need to invalidate the frame 316 // property, if it exists, at the end of the run loop. This allows it to 317 // be used inside of SC.ScrollView. 318 this.invokeLast('invalidateFrame'); 319 }, 320 321 itemTagName: function() { 322 switch(this.get('tagName')) { 323 case 'dl': 324 return 'dt'; 325 case 'ul': 326 case 'ol': 327 return 'li'; 328 case 'table': 329 case 'thead': 330 case 'tbody': 331 case 'tfoot': 332 return 'tr'; 333 case 'select': 334 return 'option'; 335 default: 336 return SC.TemplateView.prototype.tagName; 337 } 338 }.property('tagName'), 339 340 invalidateFrame: function() { 341 this.notifyPropertyChange('frame'); 342 } 343 }); 344 345