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 sc_require('models/record_attribute'); 10 11 /** @class 12 13 `SingleAttribute` is a subclass of `RecordAttribute` and handles to-one 14 relationships. 15 16 There are many ways you can configure a `SingleAttribute`: 17 18 group: SC.Record.toOne('MyApp.Group', { 19 inverse: 'contacts', // set the key used to represent the inverse 20 isMaster: YES|NO, // indicate whether changing this should dirty 21 transform: function(), // transforms value <=> storeKey, 22 isEditable: YES|NO, make editable or not 23 }); 24 25 @extends SC.RecordAttribute 26 @since SproutCore 1.0 27 */ 28 SC.SingleAttribute = SC.RecordAttribute.extend( 29 /** @scope SC.SingleAttribute.prototype */ { 30 31 /** 32 Specifies the property on the member record that represents the inverse 33 of the current relationship. If set, then modifying this relationship 34 will also alter the opposite side of the relationship. 35 36 @type String 37 @default null 38 */ 39 inverse: null, 40 41 /** 42 If set, determines that when an inverse relationship changes whether this 43 record should become dirty also or not. 44 45 @type Boolean 46 @default YES 47 */ 48 isMaster: YES, 49 50 51 /** 52 @private - implements support for handling inverse relationships. 53 */ 54 call: function(record, key, newRec) { 55 var attrKey = this.get('key') || key, 56 inverseKey, isMaster, oldRec, attr, ret, nvalue; 57 58 // WRITE 59 if (newRec !== undefined && this.get('isEditable')) { 60 61 // can only take other records or null 62 //@if(debug) 63 if (newRec && !SC.kindOf(newRec, SC.Record)) { 64 throw new Error("Developer Error: %@ is not an instance of SC.Record.".fmt(newRec)); 65 } 66 67 if (newRec && SC.none(newRec.get('id'))) { 68 throw new Error("Developer Error: Attempted to add a record without a primary key to a to-one relationship. Relationships require that the id always be specified. The record, \"%@\", must be assigned an id (i.e. be saved) before it can be used in the '%@' relationship.".fmt(newRec, attrKey)); 69 } 70 //@endif 71 72 inverseKey = this.get('inverse'); 73 if (inverseKey) oldRec = this._scsa_call(record, key); 74 75 // careful: don't overwrite value here. we want the return value to 76 // cache. 77 nvalue = this.fromType(record, key, newRec) ; // convert to attribute. 78 record.writeAttribute(attrKey, nvalue, !this.get('isMaster')); 79 ret = newRec ; 80 81 // ok, now if we have an inverse relationship, get the inverse 82 // relationship and notify it of what is happening. This will allow it 83 // to update itself as needed. The callbacks implemented here are 84 // supported by both SingleAttribute and ManyAttribute. 85 // 86 if (inverseKey && (oldRec !== newRec)) { 87 if (oldRec && (attr = oldRec[inverseKey])) { 88 attr.inverseDidRemoveRecord(oldRec, inverseKey, record, key); 89 } 90 91 if (newRec && (attr = newRec[inverseKey])) { 92 attr.inverseDidAddRecord(newRec, inverseKey, record, key); 93 } 94 } 95 96 // READ 97 } else ret = this._scsa_call(record, key, newRec); 98 99 return ret ; 100 }, 101 102 /** @private - save original call() impl */ 103 _scsa_call: SC.RecordAttribute.prototype.call, 104 105 /** 106 Called by an inverse relationship whenever the receiver is no longer part 107 of the relationship. If this matches the inverse setting of the attribute 108 then it will update itself accordingly. 109 110 @param {SC.Record} record the record owning this attribute 111 @param {String} key the key for this attribute 112 @param {SC.Record} inverseRecord record that was removed from inverse 113 @param {String} inverseKey key on inverse that was modified 114 */ 115 inverseDidRemoveRecord: function(record, key, inverseRecord, inverseKey) { 116 117 var myInverseKey = this.get('inverse'), 118 curRec = this._scsa_call(record, key), 119 isMaster = this.get('isMaster'), attr; 120 121 // ok, you removed me, I'll remove you... if isMaster, notify change. 122 record.writeAttribute(this.get('key') || key, null, !isMaster); 123 record.notifyPropertyChange(key); 124 125 // if we have another value, notify them as well... 126 if ((curRec !== inverseRecord) || (inverseKey !== myInverseKey)) { 127 if (curRec && (attr = curRec[myInverseKey])) { 128 attr.inverseDidRemoveRecord(curRec, myInverseKey, record, key); 129 } 130 } 131 }, 132 133 /** 134 Called by an inverse relationship whenever the receiver is added to the 135 inverse relationship. This will set the value of this inverse record to 136 the new record. 137 138 @param {SC.Record} record the record owning this attribute 139 @param {String} key the key for this attribute 140 @param {SC.Record} inverseRecord record that was added to inverse 141 @param {String} inverseKey key on inverse that was modified 142 */ 143 inverseDidAddRecord: function(record, key, inverseRecord, inverseKey) { 144 var myInverseKey = this.get('inverse'), 145 curRec = this._scsa_call(record, key), 146 isMaster = this.get('isMaster'), 147 attr, nvalue; 148 149 // ok, replace myself with the new value... 150 nvalue = this.fromType(record, key, inverseRecord); // convert to attr. 151 record.writeAttribute(this.get('key') || key, nvalue, !isMaster); 152 record.notifyPropertyChange(key); 153 154 // if we have another value, notify them as well... 155 if ((curRec !== inverseRecord) || (inverseKey !== myInverseKey)) { 156 if (curRec && (attr = curRec[myInverseKey])) { 157 attr.inverseDidRemoveRecord(curRec, myInverseKey, record, key); 158 } 159 } 160 } 161 162 }); 163