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