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 /** 9 @class 10 Base class for all render delegates. 11 12 You should use SC.RenderDelegate or a subclass of it as the base for all 13 of your render delegates. SC.RenderDelegate offers many helper methods 14 and can be simpler to subclass between themes than `SC.Object`. 15 16 Creating & Subclassing 17 === 18 You create render delegates just like you create SC.Objects: 19 20 MyTheme.someRenderDelegate = SC.RenderDelegate.create({ ... }); 21 22 You can subclass a render delegate and use that: 23 24 MyTheme.RenderDelegate = SC.RenderDelegate.extend({ ... }); 25 MyTheme.someRenderDelegate = MyTheme.RenderDelegate.create({}); 26 27 And you can even subclass instances or SC.RenderDelegate: 28 29 MyTheme.someRenderDelegate = SC.RenderDelegate.create({ ... }); 30 MyTheme.otherRenderDelegate = MyTheme.someRenderDelegate.create({ ... }); 31 32 // this allows you to subclass another theme's render delegate: 33 MyTheme.buttonRenderDelegate = SC.BaseTheme.buttonRenderDelegate.create({ ... }); 34 35 For render delegates, subclassing and instantiating are the same. 36 37 NOTE: Even though `.extend` and `.create` technically do the same thing, 38 convention dictates that you use `.extend` for RenderDelegates that 39 will be used primarily as base classes, and `create` for RenderDelegates 40 that you expect to be instances. 41 42 Rendering and Updating 43 === 44 Render delegates are most commonly used for two things: rendering and updating 45 DOM representations of controls. 46 47 Render delegates use their `render` and `update` methods to do this: 48 49 render: function(dataSource, context) { 50 // rendering tasks here 51 // example: 52 context.begin('div').addClass('title') 53 .text(dataSource.get('title') 54 .end(); 55 }, 56 57 update: function(dataSource, jquery) { 58 // updating tasks here 59 // example: 60 jquery.find('.title').text(dataSource.get('title')); 61 } 62 63 Variables 64 === 65 The data source provides your render delegate with all of the information 66 needed to render. However, the render delegate's consumer--usually a view-- 67 may need to get information back. 68 69 For example, `SC.AutoResize` resizes controls to fit their text. You can use 70 it to size a button to fit its title. But it can't just make the button 71 have the same width as its title: it needs to be a little larger to make room 72 for the padding to the left and right sides of the title. 73 74 This padding will vary from theme to theme. 75 76 You can specify properties on the render delegate like any other property: 77 78 MyRenderDelegate = SC.RenderDelegate.create({ 79 autoSizePadding: 10 80 ... 81 }); 82 83 But there are multiple sizes of buttons; shouldn't the padding change as 84 well? You can add hashes for the various control sizes and override properties: 85 86 SC.RenderDelegate.create({ 87 autoSizePadding: 10, 88 89 'sc-jumbo-size': { 90 autoResizePadding: 20 91 } 92 93 For details, see the discussion on size helpers below. 94 95 You can also calculate values for the data source. In this example, we calculate 96 the autoSizePadding to equal half the data source's height: 97 98 SC.RenderDelegate.create({ 99 autoSizePaddingFor: function(dataSource) { 100 if (dataSource.get('frame')) { 101 return dataSource.get('frame').height / 2; 102 } 103 } 104 105 106 When SC.ButtonView tries to get `autoSizePadding`, the render delegate will look for 107 `autoSizePaddingFor`. It will be called if it exists. Otherwise, the property will 108 be looked up like normal. 109 110 Note: To support multiple sizes, you must also render the class name; see size 111 helper discussion below. 112 113 Helpers 114 === 115 SC.RenderDelegate have "helper methods" to assist the rendering process. 116 There are a few built-in helpers, and you can add your own. 117 118 Slices 119 ---------------------- 120 Chance provides the `includeSlices` method to easily slice images for 121 use in the SproutCore theme system. 122 123 includeSlices(dataSource, context, slices); 124 125 You can call this to add DOM that matches Chance's `@include slices()` 126 directive. For example: 127 128 MyTheme.buttonRenderDelegate = SC.RenderDelegate.create({ 129 className: 'button', 130 render: function(dataSource, context) { 131 this.includeSlices(dataSource, context, SC.THREE_SLICE); 132 } 133 }); 134 135 DOM elements will be added as necessary for the slices. From your CSS, you 136 can match it like this: 137 138 $theme.button { 139 @include slices('button.png', $left: 3, $right: 3); 140 } 141 142 See the Chance documentation at http://guides.sproutcore.com/chance.html 143 for more about Chance's `@include slices` directive. 144 145 Sizing Helpers 146 ------------------------- 147 As discussed previously, you can create hashes of properties for each size. 148 However, to support sizing, you must render the size's class name. 149 150 Use the `addSizeClassName` and `updateSizeClassName` methods: 151 152 SC.RenderDelegate.create({ 153 render: function(dataSource, context) { 154 // if you want to include a class name for the control size 155 // so you can style it via CSS, include this line: 156 this.addSizeClassName(dataSource, context); 157 158 ... 159 }, 160 161 update: function(dataSource, jquery) { 162 // and don't forget to use its companion in update as well: 163 this.updateSizeClassName(dataSource, jquery); 164 165 ... 166 } 167 }); 168 169 Controls that allow multiple sizes should also be able to automatically choose 170 the correct size based on the `layout` property supplied by the user. To support 171 this, you can add properties to your size hashes: 172 173 'sc-regular-size': { 174 // to match _only_ 24px-high buttons 175 height: 24, 176 177 // or, alternatively, to match ones from 22-26: 178 minHeight: 20, maxHeight: 26, 179 180 // you can do the same for width if you wanted 181 width: 100 182 } 183 184 The correct size will be calculated automatically when `addSlizeClassName` is 185 called. If the view explicitly supplies a control size, that size will be used; 186 otherwise, it will be calculated automatically based on the properties in your 187 size hash. 188 189 Adding Custom Helpers 190 --------------------- 191 You can mix your own helpers into this base class by calling 192 SC.RenderDelegate.mixin; they will be available to all render delegates: 193 194 SC.RenderDelegate.mixin({ 195 myHelperMethod: function(dataSource) { ... } 196 }); 197 198 199 You can then use the helpers from your render delegates: 200 201 MyTheme.someRenderDelegate = SC.RenderDelegate.create({ 202 className: 'some-thingy', 203 render: function(dataSource, context) { 204 this.myHelperMethod(dataSource); 205 } 206 }); 207 208 209 By convention, all render delegate methods should take a `dataSource` as 210 their first argument. If they do any rendering or updating, their second 211 argument should be the `SC.RenderContext` or `jQuery` object to use. 212 213 In addition, helpers like these are only meant for methods that should 214 be made available to _all_ render delegates. If your method is specific 215 to just one, add it directly; if it is specific to just a few in your 216 own theme, consider just using mixins or subclassing SC.RenderDelegate: 217 218 // If you use it in a couple of render delegates, perhaps a mixin 219 // would be best: 220 MyTheme.MyRenderHelper = { 221 helper: function(dataSource) { 222 ... 223 } 224 }; 225 226 MyTheme.myRenderDelegate = SC.RenderDelegate.create(MyTheme.MyRenderHelper, { 227 render: function(dataSource, context) { ... } 228 }); 229 230 231 // If you use it in all render delegates in your theme, perhaps it 232 // would be better to create an entire subclass of 233 // SC.RenderDelegate: 234 MyTheme.RenderDelegate = SC.RenderDelegate.extend({ 235 helper: function(dataSource) { 236 ... 237 } 238 }); 239 240 MyTheme.myRenderDelegate = MyTheme.RenderDelegate.create({ 241 render: function(dataSource, context) { ... } 242 }); 243 244 Data Sources 245 === 246 Render delegates get the content to be rendered from their data sources. 247 248 A data source can be any object, so long as the object implements 249 the following methods: 250 251 - `get(propertyName)`: Returns a value for a given property. 252 - `didChangeFor(context, propertyName)`: Returns YES if any properties 253 listed have changed since the last time `didChangeFor` was called with 254 the same context. 255 256 And the following properties (to be accessed through `.get`): 257 258 - `theme`: The theme being used to render. 259 - `renderState`: An empty hash for the render delegate to save state in. 260 While render delegates are _usually_ completely stateless, there are 261 cases where they may need to save some sort of state. 262 */ 263 SC.RenderDelegate = /** @scope SC.RenderDelegate.prototype */{ 264 265 // docs will look more natural if these are all considered instance 266 // methods/properties. 267 268 /** 269 Creates a new render delegate based on this one. When you want to 270 create a render delegate, you call this: 271 272 MyTheme.myRenderDelegate = SC.RenderDelegate.create({ 273 className: 'my-render-delegate', 274 render: function(dataSource, context) { 275 // your code here... 276 } 277 }) 278 */ 279 create: function() { 280 var ret = SC.beget(this); 281 282 var idx, len = arguments.length; 283 for (idx = 0; idx < len; idx++) { 284 ret.mixin(arguments[idx]); 285 } 286 287 return ret; 288 }, 289 290 /** 291 Adds extra capabilities to this render delegate. 292 293 You can use this to add helpers to all render delegates: 294 295 SC.RenderDelegate.reopen({ 296 myHelperMethod: function(dataSource) { ... } 297 }); 298 299 */ 300 reopen: function(mixin) { 301 var i, v; 302 for (i in mixin) { 303 v = mixin[i]; 304 if (!mixin.hasOwnProperty(i)) { 305 continue; 306 } 307 308 if (typeof v === 'function' && v !== this[i]) { 309 v.base = this[i] || SC.K; 310 } 311 312 if (v && v.isEnhancement && v !== this[i]) { 313 v = SC._enhance(this[i] || SC.K, v); 314 } 315 316 this[i] = v; 317 } 318 }, 319 320 /** 321 Returns the specified property from this render delegate. 322 Implemented to match SC.Object's API. 323 */ 324 get: function(propertyName) { return this[propertyName]; }, 325 326 /** 327 Gets or generates the named property for the specified 328 dataSource. If a method `propertyName + 'For'` is found, 329 it will be used to compute the value, `dataSource` 330 being passed as an argument. Otherwise, it will simply 331 be looked up on the render delegate. 332 333 NOTE: this implementation is a reference implementation. It 334 is overridden in the sizing code (helpers/sizing.js) to be 335 size-sensitive. 336 */ 337 getPropertyFor: function(dataSource, propertyName) { 338 if (this[propertyName + 'For']) { 339 return this[propertyName + 'For'](dataSource, propertyName); 340 } 341 342 return this[propertyName]; 343 }, 344 345 /** 346 All render delegates should have a class name. Any time a render delegate is 347 used, this name should be added as a class name (`SC.View`s do this 348 automatically). 349 */ 350 className: undefined, 351 352 /** 353 Writes the DOM representation of this render delegate to the 354 supplied `SC.RenderContext`, using the supplied `dataSource` 355 for any data needed. 356 357 @method 358 @param {DataSource} dataSource An object from which to get 359 data. See documentation on data sources above. 360 @param {SC.RenderContext} context A context to render DOM into. 361 */ 362 render: function(dataSource, context) { 363 364 }, 365 366 /** 367 Updates the DOM representation of this render delegate using 368 the supplied `jQuery` instance and `dataSource`. 369 370 @method 371 @param {DataSource} dataSource An object from which to get 372 data. See documentation on data sources above. 373 @param {jQuery} jquery A jQuery instance containing the DOM 374 element to update. This will be the DOM generated by `render()`. 375 */ 376 update: function(dataSource, jQuery) { 377 378 } 379 }; 380 381 // create and extend are technically identical. 382 SC.RenderDelegate.extend = SC.RenderDelegate.create; 383 384 // and likewise, as this is both a class and an instance, mixin makes 385 // sense instead of reopen... 386 SC.RenderDelegate.mixin = SC.RenderDelegate.reopen; 387