]> git.proxmox.com Git - sencha-touch.git/blob - src/src/data/association/HasMany.js
import Sencha Touch 2.4.2 source
[sencha-touch.git] / src / src / data / association / HasMany.js
1 /**
2 * Represents a one-to-many relationship between two models. Usually created indirectly via a model definition:
3 *
4 * Ext.define('Product', {
5 * extend: 'Ext.data.Model',
6 * config: {
7 * fields: [
8 * {name: 'id', type: 'int'},
9 * {name: 'user_id', type: 'int'},
10 * {name: 'name', type: 'string'}
11 * ]
12 * }
13 * });
14 *
15 * Ext.define('User', {
16 * extend: 'Ext.data.Model',
17 * config: {
18 * fields: [
19 * {name: 'id', type: 'int'},
20 * {name: 'name', type: 'string'}
21 * ],
22 * // we can use the hasMany shortcut on the model to create a hasMany association
23 * hasMany: {model: 'Product', name: 'products'}
24 * }
25 * });
26 *
27 * Above we created Product and User models, and linked them by saying that a User hasMany Products. This gives us a new
28 * function on every User instance, in this case the function is called 'products' because that is the name we specified
29 * in the association configuration above.
30 *
31 * This new function returns a specialized {@link Ext.data.Store Store} which is automatically filtered to load only
32 * Products for the given model instance:
33 *
34 * //first, we load up a User with id of 1
35 * var user = Ext.create('User', {id: 1, name: 'Ed'});
36 *
37 * //the user.products function was created automatically by the association and returns a {@link Ext.data.Store Store}
38 * //the created store is automatically scoped to the set of Products for the User with id of 1
39 * var products = user.products();
40 *
41 * //we still have all of the usual Store functions, for example it's easy to add a Product for this User
42 * products.add({
43 * name: 'Another Product'
44 * });
45 *
46 * //saves the changes to the store - this automatically sets the new Product's user_id to 1 before saving
47 * products.sync();
48 *
49 * The new Store is only instantiated the first time you call products() to conserve memory and processing time, though
50 * calling products() a second time returns the same store instance.
51 *
52 * _Custom filtering_
53 *
54 * The Store is automatically furnished with a filter - by default this filter tells the store to only return records
55 * where the associated model's foreign key matches the owner model's primary key. For example, if a User with ID = 100
56 * hasMany Products, the filter loads only Products with user_id == 100.
57 *
58 * Sometimes we want to filter by another field - for example in the case of a Twitter search application we may have
59 * models for Search and Tweet:
60 *
61 * Ext.define('Search', {
62 * extend: 'Ext.data.Model',
63 * config: {
64 * fields: [
65 * 'id', 'query'
66 * ],
67 *
68 * hasMany: {
69 * model: 'Tweet',
70 * name : 'tweets',
71 * filterProperty: 'query'
72 * }
73 * }
74 * });
75 *
76 * Ext.define('Tweet', {
77 * extend: 'Ext.data.Model',
78 * config: {
79 * fields: [
80 * 'id', 'text', 'from_user'
81 * ]
82 * }
83 * });
84 *
85 * //returns a Store filtered by the filterProperty
86 * var store = new Search({query: 'Sencha Touch'}).tweets();
87 *
88 * The tweets association above is filtered by the query property by setting the {@link #filterProperty}, and is
89 * equivalent to this:
90 *
91 * var store = Ext.create('Ext.data.Store', {
92 * model: 'Tweet',
93 * filters: [
94 * {
95 * property: 'query',
96 * value : 'Sencha Touch'
97 * }
98 * ]
99 * });
100 *
101 * ###Further Reading
102 * [Sencha Touch Models and Associations](../../../core_concepts/data/models.html)
103 */
104 Ext.define('Ext.data.association.HasMany', {
105 extend: 'Ext.data.association.Association',
106 alternateClassName: 'Ext.data.HasManyAssociation',
107 requires: ['Ext.util.Inflector'],
108
109 alias: 'association.hasmany',
110
111 config: {
112 /**
113 * @cfg {String} foreignKey
114 * The name of the foreign key on the associated model that links it to the owner model. Defaults to the
115 * lowercased name of the owner model plus "_id", e.g. an association with a model called Group hasMany Users
116 * would create 'group_id' as the foreign key. When the remote store is loaded, the store is automatically
117 * filtered so that only records with a matching foreign key are included in the resulting child store. This can
118 * be overridden by specifying the {@link #filterProperty}.
119 *
120 * Ext.define('Group', {
121 * extend: 'Ext.data.Model',
122 * fields: ['id', 'name'],
123 * hasMany: 'User'
124 * });
125 *
126 * Ext.define('User', {
127 * extend: 'Ext.data.Model',
128 * fields: ['id', 'name', 'group_id'], // refers to the id of the group that this user belongs to
129 * belongsTo: 'Group'
130 * });
131 */
132 foreignKey: undefined,
133
134 /**
135 * @cfg {String} name
136 * The name of the function to create on the owner model to retrieve the child store. If not specified, the
137 * pluralized name of the child model is used.
138 *
139 * // This will create a users() method on any Group model instance
140 * Ext.define('Group', {
141 * extend: 'Ext.data.Model',
142 * fields: ['id', 'name'],
143 * hasMany: 'User'
144 * });
145 * var group = new Group();
146 * console.log(group.users());
147 *
148 * // The method to retrieve the users will now be getUserList
149 * Ext.define('Group', {
150 * extend: 'Ext.data.Model',
151 * fields: ['id', 'name'],
152 * hasMany: {model: 'User', name: 'getUserList'}
153 * });
154 * var group = new Group();
155 * console.log(group.getUserList());
156 */
157
158 /**
159 * @cfg {Object} store
160 * Optional configuration object that will be passed to the generated Store. Defaults to an empty Object.
161 */
162 store: undefined,
163
164 /**
165 * @cfg {String} storeName
166 * Optional The name of the store by which you can reference it on this class as a property.
167 */
168 storeName: undefined,
169
170 /**
171 * @cfg {String} filterProperty
172 * Optionally overrides the default filter that is set up on the associated Store. If this is not set, a filter
173 * is automatically created which filters the association based on the configured {@link #foreignKey}. See intro
174 * docs for more details.
175 */
176 filterProperty: null,
177
178 /**
179 * @cfg {Boolean} autoLoad
180 * `true` to automatically load the related store from a remote source when instantiated.
181 */
182 autoLoad: false,
183
184 /**
185 * @cfg {Boolean} autoSync
186 * true to automatically synchronize the related store with the remote source
187 */
188 autoSync: false
189 },
190
191 constructor: function(config) {
192 config = config || {};
193
194 if (config.storeConfig) {
195 // <debug>
196 Ext.Logger.warn('storeConfig is deprecated on an association. Instead use the store configuration.');
197 // </debug>
198 config.store = config.storeConfig;
199 delete config.storeConfig;
200 }
201
202 this.callParent([config]);
203 },
204
205 applyName: function(name) {
206 if (!name) {
207 name = Ext.util.Inflector.pluralize(this.getAssociatedName().toLowerCase());
208 }
209 return name;
210 },
211
212 applyStoreName: function(name) {
213 if (!name) {
214 name = this.getName() + 'Store';
215 }
216 return name;
217 },
218
219 applyForeignKey: function(foreignKey) {
220 if (!foreignKey) {
221 var inverse = this.getInverseAssociation();
222 if (inverse) {
223 foreignKey = inverse.getForeignKey();
224 } else {
225 foreignKey = this.getOwnerName().toLowerCase() + '_id';
226 }
227 }
228 return foreignKey;
229 },
230
231 applyAssociationKey: function(associationKey) {
232 if (!associationKey) {
233 var associatedName = this.getAssociatedName();
234 associationKey = Ext.util.Inflector.pluralize(associatedName[0].toLowerCase() + associatedName.slice(1));
235 }
236 return associationKey;
237 },
238
239 updateForeignKey: function(foreignKey, oldForeignKey) {
240 var fields = this.getAssociatedModel().getFields(),
241 field = fields.get(foreignKey);
242
243 if (!field) {
244 field = new Ext.data.Field({
245 name: foreignKey
246 });
247 fields.add(field);
248 fields.isDirty = true;
249 }
250
251 if (oldForeignKey) {
252 field = fields.get(oldForeignKey);
253 if (field) {
254 fields.remove(field);
255 fields.isDirty = true;
256 }
257 }
258 },
259
260 /**
261 * @private
262 * @deprecated as of v2.0.0 on an association. Instead use the store configuration.
263 *
264 * Creates a function that returns an Ext.data.Store which is configured to load a set of data filtered
265 * by the owner model's primary key - e.g. in a `hasMany` association where Group `hasMany` Users, this function
266 * returns a Store configured to return the filtered set of a single Group's Users.
267 * @return {Function} The store-generating function.
268 */
269 applyStore: function(storeConfig) {
270 var me = this,
271 associatedModel = me.getAssociatedModel(),
272 storeName = me.getStoreName(),
273 foreignKey = me.getForeignKey(),
274 primaryKey = me.getPrimaryKey(),
275 filterProperty = me.getFilterProperty(),
276 autoLoad = me.getAutoLoad(),
277 autoSync = me.getAutoSync();
278
279 return function() {
280 var record = this,
281 config, filter, store,
282 modelDefaults = {},
283 listeners = {
284 addrecords: me.onAddRecords,
285 removerecords: me.onRemoveRecords,
286 scope: me
287 };
288
289 if (record[storeName] === undefined) {
290 if (filterProperty) {
291 filter = {
292 property : filterProperty,
293 value : record.get(filterProperty),
294 exactMatch: true
295 };
296 } else {
297 filter = {
298 property : foreignKey,
299 value : record.get(primaryKey),
300 exactMatch: true
301 };
302 }
303
304 modelDefaults[foreignKey] = record.get(primaryKey);
305
306 config = Ext.apply({}, storeConfig, {
307 model : associatedModel,
308 filters : [filter],
309 remoteFilter : true,
310 autoSync : autoSync,
311 modelDefaults: modelDefaults
312 });
313
314 store = record[storeName] = Ext.create('Ext.data.Store', config);
315 store.boundTo = record;
316
317 store.onAfter(listeners);
318
319 if (autoLoad) {
320 record[storeName].load();
321 }
322 }
323
324 return record[storeName];
325 };
326 },
327
328 onAddRecords: function(store, records) {
329 var ln = records.length,
330 id = store.boundTo.getId(),
331 i, record;
332
333 for (i = 0; i < ln; i++) {
334 record = records[i];
335 record.set(this.getForeignKey(), id);
336 }
337 this.updateInverseInstances(store.boundTo);
338 },
339
340 onRemoveRecords: function(store, records) {
341 var ln = records.length,
342 i, record;
343 for (i = 0; i < ln; i++) {
344 record = records[i];
345 record.set(this.getForeignKey(), null);
346 }
347 },
348
349 updateStore: function(store) {
350 this.getOwnerModel().prototype[this.getName()] = store;
351 },
352
353 /**
354 * Read associated data
355 * @private
356 * @param {Ext.data.Model} record The record we're writing to.
357 * @param {Ext.data.reader.Reader} reader The reader for the associated model.
358 * @param {Object} associationData The raw associated data.
359 */
360 read: function(record, reader, associationData) {
361 var store = record[this.getName()](),
362 records = reader.read(associationData).getRecords();
363
364 store.add(records);
365 },
366
367 updateInverseInstances: function(record) {
368 var store = record[this.getName()](),
369 inverse = this.getInverseAssociation();
370
371 //if the inverse association was found, set it now on each record we've just created
372 if (inverse) {
373 store.each(function(associatedRecord) {
374 associatedRecord[inverse.getInstanceName()] = record;
375 });
376 }
377 },
378
379 getInverseAssociation: function() {
380 var ownerName = this.getOwnerModel().modelName;
381
382 //now that we've added the related records to the hasMany association, set the inverse belongsTo
383 //association on each of them if it exists
384 return this.getAssociatedModel().associations.findBy(function(assoc) {
385 return assoc.getType().toLowerCase() === 'belongsto' && assoc.getAssociatedModel().modelName === ownerName;
386 });
387 }
388
389 // <deprecated product=touch since=2.0>
390 }, function() {
391 /**
392 * @cfg {Object} storeConfig
393 * @deprecated 2.0.0 Use `store` instead.
394 */
395 Ext.deprecateProperty(this, 'storeConfig', 'store');
396 // </deprecated>
397 });