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('models/record'); 9 10 /** @class 11 12 A RecordAttribute describes a single attribute on a record. It is used to 13 generate computed properties on records that can automatically convert data 14 types and verify data. 15 16 When defining an attribute on an SC.Record, you can configure it this way: 17 18 title: SC.Record.attr(String, { 19 defaultValue: 'Untitled', 20 isRequired: YES|NO 21 }) 22 23 In addition to having predefined transform types, there is also a way to 24 set a computed relationship on an attribute. A typical example of this would 25 be if you have record with a parentGuid attribute, but are not able to 26 determine which record type to map to before looking at the guid (or any 27 other attributes). To set up such a computed property, you can attach a 28 function in the attribute definition of the SC.Record subclass: 29 30 relatedToComputed: SC.Record.toOne(function() { 31 return (this.readAttribute('relatedToComputed').indexOf("foo")==0) ? MyApp.Foo : MyApp.Bar; 32 }) 33 34 Notice that we are not using .get() to avoid another transform which would 35 trigger an infinite loop. 36 37 You usually will not work with RecordAttribute objects directly, though you 38 may extend the class in any way that you like to create a custom attribute. 39 40 A number of default RecordAttribute types are defined on the SC.Record. 41 42 @extends SC.Object 43 @see SC.Record 44 @see SC.ManyAttribute 45 @see SC.SingleAttribute 46 @since SproutCore 1.0 47 */ 48 SC.RecordAttribute = SC.Object.extend( 49 /** @scope SC.RecordAttribute.prototype */ { 50 /** 51 Walk like a duck. 52 53 @type Boolean 54 @default YES 55 */ 56 isRecordAttribute: YES, 57 58 /** 59 The default value. If attribute is `null` or `undefined`, this default 60 value will be substituted instead. Note that `defaultValue`s are not 61 converted, so the value should be in the output type expected by the 62 attribute. 63 64 If you use a `defaultValue` function, the arguments given to it are the 65 record instance and the key. 66 67 @type Object|function 68 @default null 69 */ 70 defaultValue: null, 71 72 /** 73 The attribute type. Must be either an object class or a property path 74 naming a class. The built in handler allows all native types to pass 75 through, converts records to ids and dates to UTF strings. 76 77 If you use the `attr()` helper method to create a RecordAttribute instance, 78 it will set this property to the first parameter you pass. 79 80 @type Object|String 81 @default String 82 */ 83 type: String, 84 85 /** 86 The underlying attribute key name this attribute should manage. If this 87 property is left empty, then the key will be whatever property name this 88 attribute assigned to on the record. If you need to provide some kind 89 of alternate mapping, this provides you a way to override it. 90 91 @type String 92 @default null 93 */ 94 key: null, 95 96 /** 97 If `YES`, then the attribute is required and will fail validation unless 98 the property is set to a non-null or undefined value. 99 100 @type Boolean 101 @default NO 102 */ 103 isRequired: NO, 104 105 /** 106 If `NO` then attempts to edit the attribute will be ignored. 107 108 @type Boolean 109 @default YES 110 */ 111 isEditable: YES, 112 113 /** 114 If set when using the Date format, expect the ISO8601 date format. 115 This is the default. 116 117 @type Boolean 118 @default YES 119 */ 120 useIsoDate: YES, 121 122 /** 123 Can only be used for toOne or toMany relationship attributes. If YES, 124 this flag will ensure that any related objects will also be marked 125 dirty when this record dirtied. 126 127 Useful when you might have multiple related objects that you want to 128 consider in an 'aggregated' state. For instance, by changing a child 129 object (image) you might also want to automatically mark the parent 130 (album) dirty as well. 131 132 @type Boolean 133 @default NO 134 */ 135 aggregate: NO, 136 137 138 /** 139 Can only be used for toOne or toMany relationship attributes. If YES, 140 this flag will lazily create the related record that was pushed in 141 from the data source (via pushRetrieve) if the related record does 142 not exist yet. 143 144 Useful when you have a record used as a join table. Assumptions then 145 can be made that the record exists at all times (even if it doesn't). 146 For instance, if you have a contact that is a member of groups, 147 a group will be created automatically when a contact pushes a new 148 group. 149 150 Note that you will have to take care of destroying the created record 151 once all relationships are removed from it. 152 153 @type Boolean 154 @default NO 155 */ 156 lazilyInstantiate: NO, 157 158 // .......................................................... 159 // HELPER PROPERTIES 160 // 161 162 /** 163 Returns the type, resolved to a class. If the type property is a regular 164 class, returns the type unchanged. Otherwise attempts to lookup the 165 type as a property path. 166 167 @property 168 @type Object 169 @default String 170 */ 171 typeClass: function() { 172 var ret = this.get('type'); 173 if (SC.typeOf(ret) === SC.T_STRING) ret = SC.requiredObjectForPropertyPath(ret); 174 return ret ; 175 }.property('type').cacheable(), 176 177 /** 178 Finds the transform handler. Attempts to find a transform that you 179 registered using registerTransform for this attribute's type, otherwise 180 defaults to using the default transform for String. 181 182 @property 183 @type Transform 184 */ 185 transform: function() { 186 var klass = this.get('typeClass') || String, 187 transforms = SC.RecordAttribute.transforms, 188 ret ; 189 190 // walk up class hierarchy looking for a transform handler 191 while(klass && !(ret = transforms[SC.guidFor(klass)])) { 192 // check if super has create property to detect SC.Object's 193 if(klass.superclass.hasOwnProperty('create')) klass = klass.superclass ; 194 // otherwise return the function transform handler 195 else klass = SC.T_FUNCTION ; 196 } 197 198 return ret ; 199 }.property('typeClass').cacheable(), 200 201 // .......................................................... 202 // LOW-LEVEL METHODS 203 // 204 205 /** 206 Converts the passed value into the core attribute value. This will apply 207 any format transforms. You can install standard transforms by adding to 208 the `SC.RecordAttribute.transforms` hash. See 209 SC.RecordAttribute.registerTransform() for more. 210 211 @param {SC.Record} record The record instance 212 @param {String} key The key used to access this attribute on the record 213 @param {Object} value The property value before being transformed 214 @returns {Object} The transformed value 215 */ 216 toType: function(record, key, value) { 217 var transform = this.get('transform'), 218 type = this.get('typeClass'), 219 children; 220 221 if (transform && transform.to) { 222 value = transform.to(value, this, type, record, key) ; 223 224 // if the transform needs to do something when its children change, we need to set up an observer for it 225 if(!SC.none(value) && (children = transform.observesChildren)) { 226 var i, len = children.length, 227 // store the record, transform, and key so the observer knows where it was called from 228 context = { 229 record: record, 230 key: key 231 }; 232 233 for(i = 0; i < len; i++) value.addObserver(children[i], this, this._SCRA_childObserver, context); 234 } 235 } 236 237 return value ; 238 }, 239 240 /** 241 @private 242 243 Shared observer used by any attribute whose transform creates a seperate 244 object that needs to write back to the datahash when it changes. For 245 example, when enumerable content changes on a `SC.Set` attribute, it 246 writes back automatically instead of forcing you to call `.set` manually. 247 248 This functionality can be used by setting an array named 249 observesChildren on your transform containing the names of keys to 250 observe. When one of them triggers it will call childDidChange on your 251 transform with the same arguments as to and from. 252 253 @param {Object} obj The transformed value that is being observed 254 @param {String} key The key used to access this attribute on the record 255 @param {Object} prev Previous value (not used) 256 @param {Object} context Hash of extra context information 257 */ 258 _SCRA_childObserver: function(obj, key, prev, context) { 259 // write the new value back to the record 260 this.call(context.record, context.key, obj); 261 262 // mark the attribute as dirty 263 context.record.notifyPropertyChange(context.key); 264 }, 265 266 /** 267 Converts the passed value from the core attribute value. This will apply 268 any format transforms. You can install standard transforms by adding to 269 the `SC.RecordAttribute.transforms` hash. See 270 `SC.RecordAttribute.registerTransform()` for more. 271 272 @param {SC.Record} record The record instance 273 @param {String} key The key used to access this attribute on the record 274 @param {Object} value The transformed value 275 @returns {Object} The value converted back to attribute format 276 */ 277 fromType: function(record, key, value) { 278 var transform = this.get('transform'), 279 type = this.get('typeClass'); 280 281 if (transform && transform.from) { 282 value = transform.from(value, this, type, record, key); 283 } 284 return value; 285 }, 286 287 /** 288 The core handler. Called when `get()` is called on the 289 parent record, since `SC.RecordAttribute` uses `isProperty` to masquerade 290 as a computed property. Get expects a property be a function, thus we 291 need to implement call. 292 293 @param {SC.Record} record The record instance 294 @param {String} key The key used to access this attribute on the record 295 @param {Object} value The property value if called as a setter 296 @returns {Object} property value 297 */ 298 call: function(record, key, value) { 299 var attrKey = this.get('key') || key, nvalue; 300 301 if ((value !== undefined) && this.get('isEditable')) { 302 // careful: don't overwrite value here. we want the return value to 303 // cache. 304 nvalue = this.fromType(record, key, value) ; // convert to attribute. 305 record.writeAttribute(attrKey, nvalue); 306 } 307 308 value = record.readAttribute(attrKey); 309 if (SC.none(value) && (value = this.get('defaultValue'))) { 310 if (typeof value === SC.T_FUNCTION) { 311 value = value(record, key, this); 312 } 313 } 314 315 value = this.toType(record, key, value); 316 317 return value ; 318 }, 319 320 /** 321 Apply needs to implemented for sc_super to work. 322 323 @see SC.RecordAttribute#call 324 */ 325 apply: function(target, args) { 326 return this.call.apply(target, args); 327 }, 328 329 // .......................................................... 330 // INTERNAL SUPPORT 331 // 332 333 /** @private - Make this look like a property so that `get()` will call it. */ 334 isProperty: YES, 335 336 /** @private - Make this look cacheable */ 337 isCacheable: YES, 338 339 /** @private - needed for KVO `property()` support */ 340 dependentKeys: [], 341 342 /** @private */ 343 init: function() { 344 sc_super(); 345 // setup some internal properties needed for KVO - faking 'cacheable' 346 this.cacheKey = "__cache__recattr__" + SC.guidFor(this) ; 347 this.lastSetValueKey = "__lastValue__recattr__" + SC.guidFor(this) ; 348 } 349 }) ; 350 351 // .......................................................... 352 // CLASS METHODS 353 // 354 355 SC.RecordAttribute.mixin( 356 /** @scope SC.RecordAttribute.prototype */{ 357 /** 358 The default method used to create a record attribute instance. Unlike 359 `create()`, takes an `attributeType` as the first parameter which will be 360 set on the attribute itself. You can pass a string naming a class or a 361 class itself. 362 363 @static 364 @param {Object|String} attributeType the assumed attribute type 365 @param {Hash} opts optional additional config options 366 @returns {SC.RecordAttribute} new instance 367 */ 368 attr: function(attributeType, opts) { 369 if (!opts) opts = {} ; 370 if (!opts.type) opts.type = attributeType || String ; 371 return this.create(opts); 372 }, 373 374 /** @private 375 Hash of registered transforms by class guid. 376 */ 377 transforms: {}, 378 379 /** 380 Call to register a transform handler for a specific type of object. The 381 object you pass can be of any type as long as it responds to the following 382 methods 383 384 - `to(value, attr, klass, record, key)` converts the passed value 385 (which will be of the class expected by the attribute) into the 386 underlying attribute value 387 - `from(value, attr, klass, record, key)` converts the underlying 388 attribute value into a value of the class 389 390 You can also provide an array of keys to observer on the return value. 391 When any of these change, your from method will be called to write the 392 changed object back to the record. For example: 393 394 { 395 to: function(value, attr, type, record, key) { 396 if(value) return value.toSet(); 397 else return SC.Set.create(); 398 }, 399 400 from: function(value, attr, type, record, key) { 401 return value.toArray(); 402 }, 403 404 observesChildren: ['[]'] 405 } 406 407 @static 408 @param {Object} klass the type of object you convert 409 @param {Object} transform the transform object 410 @returns {SC.RecordAttribute} receiver 411 */ 412 registerTransform: function(klass, transform) { 413 SC.RecordAttribute.transforms[SC.guidFor(klass)] = transform; 414 } 415 }); 416 417 // .......................................................... 418 // STANDARD ATTRIBUTE TRANSFORMS 419 // 420 421 // Object, String, Number just pass through. 422 423 /** @private - generic converter for Boolean records */ 424 SC.RecordAttribute.registerTransform(Boolean, { 425 /** @private - convert an arbitrary object value to a boolean */ 426 to: function(obj) { 427 return SC.none(obj) ? null : !!obj; 428 } 429 }); 430 431 /** @private - generic converter for Numbers */ 432 SC.RecordAttribute.registerTransform(Number, { 433 /** @private - convert an arbitrary object value to a Number */ 434 to: function(obj) { 435 return SC.none(obj) ? null : Number(obj) ; 436 } 437 }); 438 439 /** @private - generic converter for Strings */ 440 SC.RecordAttribute.registerTransform(String, { 441 /** @private - 442 convert an arbitrary object value to a String 443 allow null through as that will be checked separately 444 */ 445 to: function(obj) { 446 if (!(typeof obj === SC.T_STRING) && !SC.none(obj) && obj.toString) { 447 obj = obj.toString(); 448 } 449 return obj; 450 } 451 }); 452 453 /** @private - generic converter for Array */ 454 SC.RecordAttribute.registerTransform(Array, { 455 /** @private - check if obj is an array 456 */ 457 to: function(obj) { 458 if (!SC.isArray(obj) && !SC.none(obj)) { 459 obj = []; 460 } 461 return obj; 462 }, 463 464 observesChildren: ['[]'] 465 }); 466 467 /** @private - generic converter for Object */ 468 SC.RecordAttribute.registerTransform(Object, { 469 /** @private - check if obj is an object */ 470 to: function(obj) { 471 if (!(typeof obj === 'object') && !SC.none(obj)) { 472 obj = {}; 473 } 474 return obj; 475 } 476 }); 477 478 /** @private - generic converter for SC.Record-type records */ 479 SC.RecordAttribute.registerTransform(SC.Record, { 480 481 /** @private - convert a record id to a record instance */ 482 to: function(id, attr, recordType, parentRecord) { 483 var store = parentRecord.get('store'); 484 if (SC.none(id) || (id==="")) return null; 485 else return store.find(recordType, id); 486 }, 487 488 /** @private - convert a record instance to a record id */ 489 from: function(record) { return record ? record.get('id') : null; } 490 }); 491 492 /** @private - generic converter for transforming computed record attributes */ 493 SC.RecordAttribute.registerTransform(SC.T_FUNCTION, { 494 495 /** @private - convert a record id to a record instance */ 496 to: function(id, attr, recordType, parentRecord) { 497 recordType = recordType.apply(parentRecord); 498 var store = parentRecord.get('store'); 499 return store.find(recordType, id); 500 }, 501 502 /** @private - convert a record instance to a record id */ 503 from: function(record) { return record.get('id'); } 504 }); 505 506 /** @private - generic converter for Date records */ 507 SC.RecordAttribute.registerTransform(Date, { 508 509 /** @private - convert a string to a Date */ 510 to: function(str, attr) { 511 512 // If a null or undefined value is passed, don't 513 // do any normalization. 514 if (SC.none(str)) { return str; } 515 516 var ret ; 517 str = str.toString() || ''; 518 519 if (attr.get('useIsoDate')) { 520 var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" + 521 "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\\.([0-9]+))?)?" + 522 "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?", 523 d = str.match(new RegExp(regexp)), 524 offset = 0, 525 date = new Date(d[1], 0, 1), 526 time ; 527 528 if (d[3]) { date.setMonth(d[3] - 1); } 529 if (d[5]) { date.setDate(d[5]); } 530 if (d[7]) { date.setHours(d[7]); } 531 if (d[8]) { date.setMinutes(d[8]); } 532 if (d[10]) { date.setSeconds(d[10]); } 533 if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); } 534 if (d[14]) { 535 offset = (Number(d[16]) * 60) + Number(d[17]); 536 offset *= ((d[15] === '-') ? 1 : -1); 537 } 538 539 offset -= date.getTimezoneOffset(); 540 time = (Number(date) + (offset * 60 * 1000)); 541 542 ret = new Date(); 543 ret.setTime(Number(time)); 544 } else ret = new Date(Date.parse(str)); 545 return ret ; 546 }, 547 548 _dates: {}, 549 550 /** @private - pad with leading zeroes */ 551 _zeropad: function(num) { 552 return ((num<0) ? '-' : '') + ((num<10) ? '0' : '') + Math.abs(num); 553 }, 554 555 /** @private - convert a date to a string */ 556 from: function(date) { 557 558 if (SC.none(date)) { return null; } 559 560 var ret = this._dates[date.getTime()]; 561 if (ret) return ret ; 562 563 // figure timezone 564 var zp = this._zeropad, 565 tz = 0-date.getTimezoneOffset()/60; 566 567 tz = (tz === 0) ? 'Z' : '%@:00'.fmt(zp(tz)); 568 569 this._dates[date.getTime()] = ret = "%@-%@-%@T%@:%@:%@%@".fmt( 570 zp(date.getFullYear()), 571 zp(date.getMonth()+1), 572 zp(date.getDate()), 573 zp(date.getHours()), 574 zp(date.getMinutes()), 575 zp(date.getSeconds()), 576 tz) ; 577 578 return ret ; 579 } 580 }); 581 582 if (SC.DateTime && !SC.RecordAttribute.transforms[SC.guidFor(SC.DateTime)]) { 583 584 /** 585 Registers a transform to allow `SC.DateTime` to be used as a record 586 attribute, ie `SC.Record.attr(SC.DateTime);` 587 */ 588 589 SC.RecordAttribute.registerTransform(SC.DateTime, { 590 591 /** @private 592 Convert a String to a DateTime 593 */ 594 to: function(str, attr) { 595 if (SC.none(str) || SC.instanceOf(str, SC.DateTime)) return str; 596 if(attr.get('useUnixTime')) { 597 if(SC.typeOf(str) === SC.T_STRING) { str = parseInt(str); } 598 if(isNaN(str) || SC.typeOf(str) !== SC.T_NUMBER) { str = 0; } 599 return SC.DateTime.create({ milliseconds: str*1000, timezone: 0 }); 600 } 601 if (SC.instanceOf(str, Date)) return SC.DateTime.create(str.getTime()); 602 var format = attr.get('format'); 603 return SC.DateTime.parse(str, format ? format : SC.DateTime.recordFormat); 604 }, 605 606 /** @private 607 Convert a DateTime to a String 608 */ 609 from: function(dt, attr) { 610 if (SC.none(dt)) return dt; 611 if (attr.get('useUnixTime')) { 612 return dt.get('milliseconds')/1000; 613 } 614 var format = attr.get('format'); 615 return dt.toFormattedString(format ? format : SC.DateTime.recordFormat); 616 } 617 }); 618 619 } 620 621 /** 622 Parses a coreset represented as an array. 623 */ 624 SC.RecordAttribute.registerTransform(SC.Set, { 625 to: function(value, attr, type, record, key) { 626 return SC.Set.create(value); 627 }, 628 629 from: function(value, attr, type, record, key) { 630 return value.toArray(); 631 }, 632 633 observesChildren: ['[]'] 634 }); 635