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 /** 10 11 Generic base class to encode a view hierarchy. `ViewCoder`s are used to 12 collect the properties that may be included in a view design and then to 13 serialize that design to a JavaScript string that can be evaled. 14 15 To encode a view with a `ViewCoder`, simply call `SC.ViewCoder.encode(view)`. 16 Most of the time, however, you will not initiate coding directly but instead 17 work with the coder while designing an `SC.DesignerView` subclass. 18 19 ## Using a Coder 20 21 When you are passed an instance of a coder, you can simply write attributes 22 into the coder using one of the many encoding methods defined on the view. 23 Encoding methods are defined for most primitive view types. 24 25 coder.string("firstName" , "Charles").string('lastName', 'Jolley'); 26 27 @extends SC.Object 28 */ 29 SC.ObjectCoder = SC.Object.extend({ 30 31 // .......................................................... 32 // PROPERTIES 33 // 34 35 /** The `className` used to emit the design. */ 36 className: 'SC.Object', 37 38 /** 39 The method to be used to create the class or object. 40 */ 41 extendMethodName: 'extend', 42 43 /** 44 The default encoding method. If an object defines this method, then a new 45 coder will be created to encode that object. 46 */ 47 encodeMethodName: 'encode', 48 49 /** 50 The attributes that will be emitted. The values all must be strings. Use 51 one of the encoding methods defined below to actually encode attributes. 52 */ 53 attributes: null, 54 55 // .......................................................... 56 // ENCODING METHODS 57 // 58 // Call these methods to encode various types of attributes. They all take 59 // the same basic params: (key, value)... 60 61 /** 62 Utility method transforms the passed value with the passed function. 63 Handles both Arrays and individual items. 64 */ 65 transform: function(val, func) { 66 67 // for an array, transform each value with the func and then return a 68 // combined array. 69 if (SC.typeOf(val) === SC.T_ARRAY) { 70 val = val.map(function(x) { return this.transform(x, func); }, this); 71 val = '['+val+']'; 72 73 // otherwise, just call transform function on the value 74 } else { 75 val = func.call(this, val); 76 } 77 return val; 78 }, 79 80 /** 81 Encodes a string of raw JavaScript. This is the most primitive method. 82 You are expected to prep the value yourself. You can pass an array to 83 this or any other method and it will be encoded as a full array. 84 85 This method also automatically handles null and undefined values. Null 86 values are included in the output. Undefined values are ignored. 87 88 @param key {String} the key to set 89 @param val {String} the JavaScript 90 @param transform {Function} optional transform function to apply to val 91 @returns {SC.ObjectCoder} receiver 92 */ 93 js: function(key, val, transform) { 94 95 // normalize 96 if (val===undefined) { val=key; key = undefined; } 97 val = this.transform(val, function(x) { 98 return (x===null) ? "null" : transform ? transform.call(this, x) : x ; 99 }); 100 101 // save if needed. Undefined values are ignored 102 if (key !== undefined && (val !== undefined)) { 103 this.attributes[key] = val; 104 return this ; 105 } else return val ; 106 }, 107 108 /** 109 Encodes a string, wrapping it in quotes. 110 111 @param key {String} the key to set 112 @param val {String} the value 113 @returns {SC.ObjectCoder} receiver 114 */ 115 string: function(key, val) { 116 return this.js(key, val, function(x) { 117 return '"' + x.replace(/"/g, '\\"') + '"' ; 118 }); 119 }, 120 121 /** 122 Encodes a number, wrapping it in quotes. 123 124 @param key {String} the key to set 125 @param val {Number} the value 126 @returns {SC.ObjectCoder} receiver 127 */ 128 number: function(key, val) { 129 return this.js(key, val, function(x) { return x.toString(); }); 130 }, 131 132 /** 133 Encodes a bool, mapped as `YES` or `NO` 134 135 @param key {String} the key to set 136 @param val {Boolean} the value 137 @returns {SC.ObjectCoder} receiver 138 */ 139 bool: function(key, val) { 140 return this.js(key, val, function(x) { return x ? "true" : "false"; }); 141 }, 142 143 /** 144 Encodes an object. This will do its best to autodetect the type of the 145 object. You can pass an optional processing function that will be used 146 on object members before processing to allow you to normalize. The 147 method signature must be: 148 149 function convert(value, rootObject, key); 150 151 The rootObject and key will be set to give you the context in the 152 hierarchy. 153 154 Generally this method will work for encoding simple value only. If your 155 object graph may contain SproutCore objects, you will need to encode it 156 yourself. 157 158 @param key {String} the key to set 159 @param val {Object} the value 160 @param func {Function} optional transform func 161 @returns {SC.ObjectCoder} receiver 162 */ 163 encode: function(key, val, func) { 164 // normalize params 165 if (func===undefined && val instanceof Function) { 166 func = val; val = key; key = undefined; 167 } 168 169 return this.js(key, val, function(cur) { 170 if (func) cur = func.call(this, cur, null, null); 171 switch(SC.typeOf(cur)) { 172 case SC.T_STRING: 173 cur = this.string(cur); 174 break; 175 176 case SC.T_NUMBER: 177 cur = this.number(cur); 178 break; 179 180 case SC.T_BOOL: 181 cur = this.bool(cur); 182 break; 183 184 case SC.T_ARRAY: 185 cur = this.array(cur, func) ; 186 break; 187 188 case SC.T_HASH: 189 cur = this.hash(cur, func); 190 break ; 191 192 default: 193 // otherwise, if the object has a designer attached, try to encode 194 // view. 195 cur = cur ? this.object(cur) : this.js(cur); 196 } 197 return cur ; 198 }); 199 }, 200 201 /** 202 Encodes a hash of objects. The object values must be simple objects for 203 this method to work. You can also optionally pass a processing function 204 that will be invoked for each value, giving you a chance to convert the 205 value first. The signature must be `(key, value, rootObject)`. 206 207 @param key {String} the key to set 208 @param val {Object} the value 209 @param func {Function} optional transform func 210 @returns {SC.ObjectCoder} receiver 211 */ 212 hash: function(key, val, func) { 213 214 // normalize params 215 if (func===undefined && val instanceof Function) { 216 func = val; val = key; key = undefined; 217 } 218 219 return this.js(key, val, function(x) { 220 var ret = [] ; 221 for(var key in x) { 222 if (!x.hasOwnProperty(key)) continue; // only include added... 223 ret.push("%@: %@".fmt(this.encode(key), this.encode(x[key], func))); 224 } 225 return "{%@}".fmt(ret.join(",")); 226 }); 227 }, 228 229 /** 230 Encodes a array of objects. The object values must be simple objects for 231 this method to work. You can also optionally pass a processing function 232 that will be invoked for each value, giving you a chance to convert the 233 value first. The signature must be `(index, value, rootObject)`. 234 235 @param key {String} the key to set 236 @param val {Object} the value 237 @param func {Function} optional transform func 238 @returns {SC.ObjectCoder} receiver 239 */ 240 array: function(key, val, func) { 241 242 // normalize params 243 if (func===undefined && val instanceof Function) { 244 func = val; val = key; key = undefined; 245 } 246 247 val = val.map(function(x) { return this.encode(x, func); }, this); 248 val = "[%@]".fmt(val.join(",")); 249 250 return this.js(key, val); 251 }, 252 253 /** 254 Attempts to encode an object. The object must implement the 255 encodeMethodName for this encoder, or else an exception will be raised. 256 257 @param key {String} the key to set 258 @param val {Object} the object to encode 259 @returns {SC.ObjectCoder} receiver 260 */ 261 object: function(key, val) { 262 return this.js(key, val, function(x) { 263 return this.constructor.encode(x, this); 264 }); 265 }, 266 267 // .......................................................... 268 // INTERNAL SUPPORT 269 // 270 271 spaces: function() { 272 var spaces = this.context ? this.context.get('spaces') : '' ; 273 spaces = spaces + ' '; 274 return spaces ; 275 }.property().cacheable(), 276 277 /** 278 Emits the final JavaScript output for this coder based on the current 279 attributes. 280 */ 281 emit: function() { 282 283 // return undefined if the encoding was rejected... 284 if (this.invalid) return undefined ; 285 286 var ret = [], attrs = this.attributes, key ; 287 var methodName = this.get('extendMethodName'); 288 var spaces = this.get('spaces'); 289 290 // compute attribute body... 291 for(key in attrs) { 292 if (!attrs.hasOwnProperty(key)) continue ; 293 ret.push("%@: %@".fmt(key, attrs[key])); 294 } 295 296 if (ret.length <= 0) { 297 return "%@1%@2.%@3({})".fmt(spaces, this.className, methodName); 298 } else { 299 // handle NO class formatting.. 300 ret = ret.join(","); 301 return "%@2.%@3({%@4})".fmt(spaces, this.className, methodName, ret); 302 } 303 }, 304 305 /** 306 Begins encoding with a particular object, setting the className to the 307 object's `className`. This is used internally by the `encode()` method. 308 */ 309 begin: function(object) { 310 var methodName = this.get('encodeMethodName'); 311 if (SC.typeOf(object[methodName]) !== SC.T_FUNCTION) { 312 SC.throw("Cannot encode %@ because it does not respond to %@()".fmt(object, methodName)); 313 } 314 315 // save className for later coding 316 this.set('className', SC._object_className(object.constructor)); 317 318 // then call encode method... 319 var ret = object[methodName](this); 320 321 // if encoding method returns NO, then encoding is not allowed. 322 // note that returning void should count as YES. 323 this.invalid = ret === NO ; 324 325 // and return this 326 return this ; 327 }, 328 329 init: function() { 330 sc_super(); 331 this.set('attributes', {}); 332 }, 333 334 destroy: function() { 335 sc_super(); 336 this.context = this.className = this.attributes = null ; // cleanup 337 } 338 339 }); 340 341 SC.ObjectCoder.encode = function(object, context) { 342 // create coder and emit code... 343 var coder = this.create({ context: context }); 344 var ret = coder.begin(object).emit(); 345 346 // cleanup and return 347 coder.destroy(); 348 return ret ; 349 } ; 350