Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / data / BelongsToAssociation.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @author Ed Spencer
17  * @class Ext.data.BelongsToAssociation
18  * @extends Ext.data.Association
19  *
20  * Represents a many to one association with another model. The owner model is expected to have
21  * a foreign key which references the primary key of the associated model:
22  *
23  *     Ext.define('Category', {
24  *         extend: 'Ext.data.Model',
25  *         fields: [
26  *             { name: 'id',   type: 'int' },
27  *             { name: 'name', type: 'string' }
28  *         ]
29  *     });
30  *
31  *     Ext.define('Product', {
32  *         extend: 'Ext.data.Model',
33  *         fields: [
34  *             { name: 'id',          type: 'int' },
35  *             { name: 'category_id', type: 'int' },
36  *             { name: 'name',        type: 'string' }
37  *         ],
38  *         // we can use the belongsTo shortcut on the model to create a belongsTo association
39  *         associations: [
40  *             { type: 'belongsTo', model: 'Category' }
41  *         ]
42  *     });
43  *
44  * In the example above we have created models for Products and Categories, and linked them together
45  * by saying that each Product belongs to a Category. This automatically links each Product to a Category
46  * based on the Product's category_id, and provides new functions on the Product model:
47  *
48  * ## Generated getter function
49  *
50  * The first function that is added to the owner model is a getter function:
51  *
52  *     var product = new Product({
53  *         id: 100,
54  *         category_id: 20,
55  *         name: 'Sneakers'
56  *     });
57  *
58  *     product.getCategory(function(category, operation) {
59  *         // do something with the category object
60  *         alert(category.get('id')); // alerts 20
61  *     }, this);
62  *
63  * The getCategory function was created on the Product model when we defined the association. This uses the
64  * Category's configured {@link Ext.data.proxy.Proxy proxy} to load the Category asynchronously, calling the provided
65  * callback when it has loaded.
66  *
67  * The new getCategory function will also accept an object containing success, failure and callback properties
68  * - callback will always be called, success will only be called if the associated model was loaded successfully
69  * and failure will only be called if the associatied model could not be loaded:
70  *
71  *     product.getCategory({
72  *         callback: function(category, operation) {}, // a function that will always be called
73  *         success : function(category, operation) {}, // a function that will only be called if the load succeeded
74  *         failure : function(category, operation) {}, // a function that will only be called if the load did not succeed
75  *         scope   : this // optionally pass in a scope object to execute the callbacks in
76  *     });
77  *
78  * In each case above the callbacks are called with two arguments - the associated model instance and the
79  * {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is
80  * useful when the instance could not be loaded.
81  *
82  * ## Generated setter function
83  *
84  * The second generated function sets the associated model instance - if only a single argument is passed to
85  * the setter then the following two calls are identical:
86  *
87  *     // this call...
88  *     product.setCategory(10);
89  *
90  *     // is equivalent to this call:
91  *     product.set('category_id', 10);
92  *
93  * If we pass in a second argument, the model will be automatically saved and the second argument passed to
94  * the owner model's {@link Ext.data.Model#save save} method:
95  *
96  *     product.setCategory(10, function(product, operation) {
97  *         // the product has been saved
98  *         alert(product.get('category_id')); //now alerts 10
99  *     });
100  *
101  *     //alternative syntax:
102  *     product.setCategory(10, {
103  *         callback: function(product, operation), // a function that will always be called
104  *         success : function(product, operation), // a function that will only be called if the load succeeded
105  *         failure : function(product, operation), // a function that will only be called if the load did not succeed
106  *         scope   : this //optionally pass in a scope object to execute the callbacks in
107  *     })
108  *
109  * ## Customisation
110  *
111  * Associations reflect on the models they are linking to automatically set up properties such as the
112  * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:
113  *
114  *     Ext.define('Product', {
115  *         fields: [...],
116  *
117  *         associations: [
118  *             { type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id' }
119  *         ]
120  *     });
121  *
122  * Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'category_id')
123  * with our own settings. Usually this will not be needed.
124  */
125 Ext.define('Ext.data.BelongsToAssociation', {
126     extend: 'Ext.data.Association',
127
128     alias: 'association.belongsto',
129
130     /**
131      * @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated
132      * model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a
133      * model called Product would set up a product_id foreign key.
134      *
135      *     Ext.define('Order', {
136      *         extend: 'Ext.data.Model',
137      *         fields: ['id', 'date'],
138      *         hasMany: 'Product'
139      *     });
140      *
141      *     Ext.define('Product', {
142      *         extend: 'Ext.data.Model',
143      *         fields: ['id', 'name', 'order_id'], // refers to the id of the order that this product belongs to
144      *         belongsTo: 'Group'
145      *     });
146      *     var product = new Product({
147      *         id: 1,
148      *         name: 'Product 1',
149      *         order_id: 22
150      *     }, 1);
151      *     product.getOrder(); // Will make a call to the server asking for order_id 22
152      *
153      */
154
155     /**
156      * @cfg {String} getterName The name of the getter function that will be added to the local model's prototype.
157      * Defaults to 'get' + the name of the foreign model, e.g. getCategory
158      */
159
160     /**
161      * @cfg {String} setterName The name of the setter function that will be added to the local model's prototype.
162      * Defaults to 'set' + the name of the foreign model, e.g. setCategory
163      */
164
165     /**
166      * @cfg {String} type The type configuration can be used when creating associations using a configuration object.
167      * Use 'belongsTo' to create a HasManyAssocation
168      *
169      *     associations: [{
170      *         type: 'belongsTo',
171      *         model: 'User'
172      *     }]
173      */
174     constructor: function(config) {
175         this.callParent(arguments);
176
177         var me             = this,
178             ownerProto     = me.ownerModel.prototype,
179             associatedName = me.associatedName,
180             getterName     = me.getterName || 'get' + associatedName,
181             setterName     = me.setterName || 'set' + associatedName;
182
183         Ext.applyIf(me, {
184             name        : associatedName,
185             foreignKey  : associatedName.toLowerCase() + "_id",
186             instanceName: associatedName + 'BelongsToInstance',
187             associationKey: associatedName.toLowerCase()
188         });
189
190         ownerProto[getterName] = me.createGetter();
191         ownerProto[setterName] = me.createSetter();
192     },
193
194     /**
195      * @private
196      * Returns a setter function to be placed on the owner model's prototype
197      * @return {Function} The setter function
198      */
199     createSetter: function() {
200         var me              = this,
201             ownerModel      = me.ownerModel,
202             associatedModel = me.associatedModel,
203             foreignKey      = me.foreignKey,
204             primaryKey      = me.primaryKey;
205
206         //'this' refers to the Model instance inside this function
207         return function(value, options, scope) {
208             this.set(foreignKey, value);
209
210             if (typeof options == 'function') {
211                 options = {
212                     callback: options,
213                     scope: scope || this
214                 };
215             }
216
217             if (Ext.isObject(options)) {
218                 return this.save(options);
219             }
220         };
221     },
222
223     /**
224      * @private
225      * Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance
226      * the first time it is loaded so that subsequent calls to the getter always receive the same reference.
227      * @return {Function} The getter function
228      */
229     createGetter: function() {
230         var me              = this,
231             ownerModel      = me.ownerModel,
232             associatedName  = me.associatedName,
233             associatedModel = me.associatedModel,
234             foreignKey      = me.foreignKey,
235             primaryKey      = me.primaryKey,
236             instanceName    = me.instanceName;
237
238         //'this' refers to the Model instance inside this function
239         return function(options, scope) {
240             options = options || {};
241
242             var model = this,
243                 foreignKeyId = model.get(foreignKey),
244                 instance,
245                 args;
246
247             if (model[instanceName] === undefined) {
248                 instance = Ext.ModelManager.create({}, associatedName);
249                 instance.set(primaryKey, foreignKeyId);
250
251                 if (typeof options == 'function') {
252                     options = {
253                         callback: options,
254                         scope: scope || model
255                     };
256                 }
257
258                 associatedModel.load(foreignKeyId, options);
259                 model[instanceName] = associatedModel;
260                 return associatedModel;
261             } else {
262                 instance = model[instanceName];
263                 args = [instance];
264                 scope = scope || model;
265
266                 //TODO: We're duplicating the callback invokation code that the instance.load() call above
267                 //makes here - ought to be able to normalize this - perhaps by caching at the Model.load layer
268                 //instead of the association layer.
269                 Ext.callback(options, scope, args);
270                 Ext.callback(options.success, scope, args);
271                 Ext.callback(options.failure, scope, args);
272                 Ext.callback(options.callback, scope, args);
273
274                 return instance;
275             }
276         };
277     },
278
279     /**
280      * Read associated data
281      * @private
282      * @param {Ext.data.Model} record The record we're writing to
283      * @param {Ext.data.reader.Reader} reader The reader for the associated model
284      * @param {Object} associationData The raw associated data
285      */
286     read: function(record, reader, associationData){
287         record[this.instanceName] = reader.read([associationData]).records[0];
288     }
289 });
290