/** * @author Ed Spencer * @class Ext.data.Model * * <p>A Model represents some object that your application manages. For example, one might define a Model for Users, Products, * Cars, or any other real-world object that we want to model in the system. Models are registered via the {@link Ext.ModelManager model manager}, * and are used by {@link Ext.data.Store stores}, which are in turn used by many of the data-bound components in Ext.</p> * * <p>Models are defined as a set of fields and any arbitrary methods and properties relevant to the model. For example:</p> * <pre><code> Ext.define('User', { extend: 'Ext.data.Model', fields: [ {name: 'name', type: 'string'}, {name: 'age', type: 'int'}, {name: 'phone', type: 'string'}, {name: 'alive', type: 'boolean', defaultValue: true} ], changeName: function() { var oldName = this.get('name'), newName = oldName + " The Barbarian"; this.set('name', newName); } }); </code></pre> * * <p>The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link Ext.ModelManager ModelManager}, and all * other functions and properties are copied to the new Model's prototype.</p> * * <p>Now we can create instances of our User model and call any model logic we defined:</p> * <pre><code> var user = Ext.ModelManager.create({ name : 'Conan', age : 24, phone: '555-555-5555' }, 'User'); user.changeName(); user.get('name'); //returns "Conan The Barbarian" </code></pre> * * <p><u>Validations</u></p> * * <p>Models have built-in support for validations, which are executed against the validator functions in * {@link Ext.data.validations} ({@link Ext.data.validations see all validation functions}). Validations are easy to add to models:</p> * <pre><code> Ext.define('User', { extend: 'Ext.data.Model', fields: [ {name: 'name', type: 'string'}, {name: 'age', type: 'int'}, {name: 'phone', type: 'string'}, {name: 'gender', type: 'string'}, {name: 'username', type: 'string'}, {name: 'alive', type: 'boolean', defaultValue: true} ], validations: [ {type: 'presence', field: 'age'}, {type: 'length', field: 'name', min: 2}, {type: 'inclusion', field: 'gender', list: ['Male', 'Female']}, {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']}, {type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/} ] }); </code></pre> * * <p>The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors} * object:</p> * <pre><code> var instance = Ext.ModelManager.create({ name: 'Ed', gender: 'Male', username: 'edspencer' }, 'User'); var errors = instance.validate(); </code></pre> * * <p><u>Associations</u></p> * * <p>Models can have associations with other Models via {@link Ext.data.BelongsToAssociation belongsTo} and * {@link Ext.data.HasManyAssociation hasMany} associations. For example, let's say we're writing a blog administration * application which deals with Users, Posts and Comments. We can express the relationships between these models like this:</p> * <pre><code> Ext.define('Post', { extend: 'Ext.data.Model', fields: ['id', 'user_id'], belongsTo: 'User', hasMany : {model: 'Comment', name: 'comments'} }); Ext.define('Comment', { extend: 'Ext.data.Model', fields: ['id', 'user_id', 'post_id'], belongsTo: 'Post' }); Ext.define('User', { extend: 'Ext.data.Model', fields: ['id'], hasMany: [ 'Post', {model: 'Comment', name: 'comments'} ] }); </code></pre> * * <p>See the docs for {@link Ext.data.BelongsToAssociation} and {@link Ext.data.HasManyAssociation} for details on the usage * and configuration of associations. Note that associations can also be specified like this:</p> * <pre><code> Ext.define('User', { extend: 'Ext.data.Model', fields: ['id'], associations: [ {type: 'hasMany', model: 'Post', name: 'posts'}, {type: 'hasMany', model: 'Comment', name: 'comments'} ] }); </code></pre> * * <p><u>Using a Proxy</u></p> * * <p>Models are great for representing types of data and relationships, but sooner or later we're going to want to * load or save that data somewhere. All loading and saving of data is handled via a {@link Ext.data.proxy.Proxy Proxy}, * which can be set directly on the Model:</p> * <pre><code> Ext.define('User', { extend: 'Ext.data.Model', fields: ['id', 'name', 'email'], proxy: { type: 'rest', url : '/users' } }); </code></pre> * * <p>Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save data to and from a * RESTful backend. Let's see how this works:</p> * <pre><code> var user = Ext.ModelManager.create({name: 'Ed Spencer', email: 'ed@sencha.com'}, 'User'); user.save(); //POST /users </code></pre> * * <p>Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to persist this * Model's data onto our server. RestProxy figures out that this Model hasn't been saved before because it doesn't * have an id, and performs the appropriate action - in this case issuing a POST request to the url we configured * (/users). We configure any Proxy on any Model and always follow this API - see {@link Ext.data.proxy.Proxy} for a full * list.</p> * * <p>Loading data via the Proxy is equally easy:</p> * <pre><code> //get a reference to the User model class var User = Ext.ModelManager.getModel('User'); //Uses the configured RestProxy to make a GET request to /users/123 User.load(123, { success: function(user) { console.log(user.getId()); //logs 123 } }); </code></pre> * * <p>Models can also be updated and destroyed easily:</p> * <pre><code> //the user Model we loaded in the last snippet: user.set('name', 'Edward Spencer'); //tells the Proxy to save the Model. In this case it will perform a PUT request to /users/123 as this Model already has an id user.save({ success: function() { console.log('The User was updated'); } }); //tells the Proxy to destroy the Model. Performs a DELETE request to /users/123 user.destroy({ success: function() { console.log('The User was destroyed!'); } }); </code></pre> * * <p><u>Usage in Stores</u></p> * * <p>It is very common to want to load a set of Model instances to be displayed and manipulated in the UI. We do this * by creating a {@link Ext.data.Store Store}:</p> * <pre><code> var store = new Ext.data.Store({ model: 'User' }); //uses the Proxy we set up on Model to load the Store data store.load(); </code></pre> * * <p>A Store is just a collection of Model instances - usually loaded from a server somewhere. Store can also maintain * a set of added, updated and removed Model instances to be synchronized with the server via the Proxy. See the * {@link Ext.data.Store Store docs} for more information on Stores.</p> * * @constructor * @param {Object} data An object containing keys corresponding to this model's fields, and their associated values * @param {Number} id Optional unique ID to assign to this model instance */ Ext.define('Ext.data.Model', { alternateClassName: 'Ext.data.Record', mixins: { observable: 'Ext.util.Observable' }, requires: [ 'Ext.ModelManager', 'Ext.data.Field', 'Ext.data.Errors', 'Ext.data.Operation', 'Ext.data.validations', 'Ext.data.proxy.Ajax', 'Ext.util.MixedCollection' ], onClassExtended: function(cls, data) { var onBeforeClassCreated = data.onBeforeClassCreated; data.onBeforeClassCreated = function(cls, data) { var me = this, name = Ext.getClassName(cls), prototype = cls.prototype, superCls = cls.prototype.superclass, validations = data.validations || [], fields = data.fields || [], associations = data.associations || [], belongsTo = data.belongsTo, hasMany = data.hasMany, fieldsMixedCollection = new Ext.util.MixedCollection(false, function(field) { return field.name; }), associationsMixedCollection = new Ext.util.MixedCollection(false, function(association) { return association.name; }), superValidations = superCls.validations, superFields = superCls.fields, superAssociations = superCls.associations, association, i, ln, dependencies = []; // Save modelName on class and its prototype cls.modelName = name; prototype.modelName = name; // Merge the validations of the superclass and the new subclass if (superValidations) { validations = superValidations.concat(validations); } data.validations = validations; // Merge the fields of the superclass and the new subclass if (superFields) { fields = superFields.items.concat(fields); } for (i = 0, ln = fields.length; i < ln; ++i) { fieldsMixedCollection.add(new Ext.data.Field(fields[i])); } data.fields = fieldsMixedCollection; //associations can be specified in the more convenient format (e.g. not inside an 'associations' array). //we support that here if (belongsTo) { belongsTo = Ext.Array.from(belongsTo); for (i = 0, ln = belongsTo.length; i < ln; ++i) { association = belongsTo[i]; if (!Ext.isObject(association)) { association = {model: association}; } association.type = 'belongsTo'; associations.push(association); } delete data.belongsTo; } if (hasMany) { hasMany = Ext.Array.from(hasMany); for (i = 0, ln = hasMany.length; i < ln; ++i) { association = hasMany[i]; if (!Ext.isObject(association)) { association = {model: association}; } association.type = 'hasMany'; associations.push(association); } delete data.hasMany; } if (superAssociations) { associations = superAssociations.items.concat(associations); } for (i = 0, ln = associations.length; i < ln; ++i) { dependencies.push('association.' + associations[i].type.toLowerCase()); } if (data.proxy) { if (typeof data.proxy === 'string') { dependencies.push('proxy.' + data.proxy); } else if (typeof data.proxy.type === 'string') { dependencies.push('proxy.' + data.proxy.type); } } Ext.require(dependencies, function() { Ext.ModelManager.registerType(name, cls); for (i = 0, ln = associations.length; i < ln; ++i) { association = associations[i]; Ext.apply(association, { ownerModel: name, associatedModel: association.model }); if (Ext.ModelManager.getModel(association.model) === undefined) { Ext.ModelManager.registerDeferredAssociation(association); } else { associationsMixedCollection.add(Ext.data.Association.create(association)); } } data.associations = associationsMixedCollection; onBeforeClassCreated.call(me, cls, data); cls.setProxy(cls.prototype.proxy || cls.prototype.defaultProxyType); // Fire the onModelDefined template method on ModelManager Ext.ModelManager.onModelDefined(cls); }); }; }, inheritableStatics: { /** * Sets the Proxy to use for this model. Accepts any options that can be accepted by {@link Ext#createByAlias Ext.createByAlias} * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy * @static */ setProxy: function(proxy) { //make sure we have an Ext.data.proxy.Proxy object if (!proxy.isProxy) { if (typeof proxy == "string") { proxy = { type: proxy }; } proxy = Ext.createByAlias("proxy." + proxy.type, proxy); } proxy.setModel(this); this.proxy = this.prototype.proxy = proxy; return proxy; }, /** * Returns the configured Proxy for this Model * @return {Ext.data.proxy.Proxy} The proxy */ getProxy: function() { return this.proxy; }, /** * <b>Static</b>. Asynchronously loads a model instance by id. Sample usage: <pre><code> MyApp.User = Ext.define('User', { extend: 'Ext.data.Model', fields: [ {name: 'id', type: 'int'}, {name: 'name', type: 'string'} ] }); MyApp.User.load(10, { scope: this, failure: function(record, operation) { //do something if the load failed }, success: function(record, operation) { //do something if the load succeeded }, callback: function(record, operation) { //do something whether the load succeeded or failed } }); </code></pre> * @param {Number} id The id of the model to load * @param {Object} config Optional config object containing success, failure and callback functions, plus optional scope * @member Ext.data.Model * @method load * @static */ load: function(id, config) { config = Ext.apply({}, config); config = Ext.applyIf(config, { action: 'read', id : id }); var operation = Ext.create('Ext.data.Operation', config), scope = config.scope || this, record = null, callback; callback = function(operation) { if (operation.wasSuccessful()) { record = operation.getRecords()[0]; Ext.callback(config.success, scope, [record, operation]); } else { Ext.callback(config.failure, scope, [record, operation]); } Ext.callback(config.callback, scope, [record, operation]); }; this.proxy.read(operation, callback, this); } }, statics: { PREFIX : 'ext-record', AUTO_ID: 1, EDIT : 'edit', REJECT : 'reject', COMMIT : 'commit', /** * Generates a sequential id. This method is typically called when a record is {@link #create}d * and {@link #Record no id has been specified}. The id will automatically be assigned * to the record. The returned id takes the form: * <tt>{PREFIX}-{AUTO_ID}</tt>.<div class="mdetail-params"><ul> * <li><b><tt>PREFIX</tt></b> : String<p class="sub-desc"><tt>Ext.data.Model.PREFIX</tt> * (defaults to <tt>'ext-record'</tt>)</p></li> * <li><b><tt>AUTO_ID</tt></b> : String<p class="sub-desc"><tt>Ext.data.Model.AUTO_ID</tt> * (defaults to <tt>1</tt> initially)</p></li> * </ul></div> * @param {Ext.data.Model} rec The record being created. The record does not exist, it's a {@link #phantom}. * @return {String} auto-generated string id, <tt>"ext-record-i++'</tt>; * @static */ id: function(rec) { var id = [this.PREFIX, '-', this.AUTO_ID++].join(''); rec.phantom = true; rec.internalId = id; return id; } }, /** * Internal flag used to track whether or not the model instance is currently being edited. Read-only * @property editing * @type Boolean */ editing : false, /** * Readonly flag - true if this Record has been modified. * @type Boolean */ dirty : false, /** * @cfg {String} persistanceProperty The property on this Persistable object that its data is saved to. * Defaults to 'data' (e.g. all persistable data resides in this.data.) */ persistanceProperty: 'data', evented: false, isModel: true, /** * <tt>true</tt> when the record does not yet exist in a server-side database (see * {@link #setDirty}). Any record which has a real database pk set as its id property * is NOT a phantom -- it's real. * @property phantom * @type {Boolean} */ phantom : false, /** * @cfg {String} idProperty The name of the field treated as this Model's unique id (defaults to 'id'). */ idProperty: 'id', /** * The string type of the default Model Proxy. Defaults to 'ajax' * @property defaultProxyType * @type String */ defaultProxyType: 'ajax', /** * An array of the fields defined on this model * @property fields * @type {Array} */ // raw not documented intentionally, meant to be used internally. constructor: function(data, id, raw) { data = data || {}; var me = this, fields, length, field, name, i, isArray = Ext.isArray(data), newData = isArray ? {} : null; // to hold mapped array data if needed /** * An internal unique ID for each Model instance, used to identify Models that don't have an ID yet * @property internalId * @type String * @private */ me.internalId = (id || id === 0) ? id : Ext.data.Model.id(me); /** * The raw data used to create this model if created via a reader. * @property raw * @type Object */ me.raw = raw; Ext.applyIf(me, { data: {} }); /** * Key: value pairs of all fields whose values have changed * @property modified * @type Object */ me.modified = {}; me[me.persistanceProperty] = {}; me.mixins.observable.constructor.call(me); //add default field values if present fields = me.fields.items; length = fields.length; for (i = 0; i < length; i++) { field = fields[i]; name = field.name; if (isArray){ // Have to map array data so the values get assigned to the named fields // rather than getting set as the field names with undefined values. newData[name] = data[i]; } else if (data[name] === undefined) { data[name] = field.defaultValue; } } me.set(newData || data); // clear any dirty/modified since we're initializing me.dirty = false; me.modified = {}; if (me.getId()) { me.phantom = false; } if (typeof me.init == 'function') { me.init(); } me.id = me.modelName + '-' + me.internalId; Ext.ModelManager.register(me); }, /** * Returns the value of the given field * @param {String} fieldName The field to fetch the value for * @return {Mixed} The value */ get: function(field) { return this[this.persistanceProperty][field]; }, /** * Sets the given field to the given value, marks the instance as dirty * @param {String|Object} fieldName The field to set, or an object containing key/value pairs * @param {Mixed} value The value to set */ set: function(fieldName, value) { var me = this, fields = me.fields, modified = me.modified, convertFields = [], field, key, i, currentValue; /* * If we're passed an object, iterate over that object. NOTE: we pull out fields with a convert function and * set those last so that all other possible data is set before the convert function is called */ if (arguments.length == 1 && Ext.isObject(fieldName)) { for (key in fieldName) { if (fieldName.hasOwnProperty(key)) { //here we check for the custom convert function. Note that if a field doesn't have a convert function, //we default it to its type's convert function, so we have to check that here. This feels rather dirty. field = fields.get(key); if (field && field.convert !== field.type.convert) { convertFields.push(key); continue; } me.set(key, fieldName[key]); } } for (i = 0; i < convertFields.length; i++) { field = convertFields[i]; me.set(field, fieldName[field]); } } else { if (fields) { field = fields.get(fieldName); if (field && field.convert) { value = field.convert(value, me); } } currentValue = me.get(fieldName); me[me.persistanceProperty][fieldName] = value; if (field && field.persist && !me.isEqual(currentValue, value)) { me.dirty = true; me.modified[fieldName] = currentValue; } if (!me.editing) { me.afterEdit(); } } }, /** * Checks if two values are equal, taking into account certain * special factors, for example dates. * @private * @param {Object} a The first value * @param {Object} b The second value * @return {Boolean} True if the values are equal */ isEqual: function(a, b){ if (Ext.isDate(a) && Ext.isDate(b)) { return a.getTime() === b.getTime(); } return a === b; }, /** * Begin an edit. While in edit mode, no events (e.g.. the <code>update</code> event) * are relayed to the containing store. When an edit has begun, it must be followed * by either {@link #endEdit} or {@link #cancelEdit}. */ beginEdit : function(){ var me = this; if (!me.editing) { me.editing = true; me.dirtySave = me.dirty; me.dataSave = Ext.apply({}, me[me.persistanceProperty]); me.modifiedSave = Ext.apply({}, me.modified); } }, /** * Cancels all changes made in the current edit operation. */ cancelEdit : function(){ var me = this; if (me.editing) { me.editing = false; // reset the modified state, nothing changed since the edit began me.modified = me.modifiedSave; me[me.persistanceProperty] = me.dataSave; me.dirty = me.dirtySave; delete me.modifiedSave; delete me.dataSave; delete me.dirtySave; } }, /** * End an edit. If any data was modified, the containing store is notified * (ie, the store's <code>update</code> event will fire). * @param {Boolean} silent True to not notify the store of the change */ endEdit : function(silent){ var me = this; if (me.editing) { me.editing = false; delete me.modifiedSave; delete me.dataSave; delete me.dirtySave; if (silent !== true && me.dirty) { me.afterEdit(); } } }, /** * Gets a hash of only the fields that have been modified since this Model was created or commited. * @return Object */ getChanges : function(){ var modified = this.modified, changes = {}, field; for (field in modified) { if (modified.hasOwnProperty(field)){ changes[field] = this.get(field); } } return changes; }, /** * Returns <tt>true</tt> if the passed field name has been <code>{@link #modified}</code> * since the load or last commit. * @param {String} fieldName {@link Ext.data.Field#name} * @return {Boolean} */ isModified : function(fieldName) { return this.modified.hasOwnProperty(fieldName); }, /** * <p>Marks this <b>Record</b> as <code>{@link #dirty}</code>. This method * is used interally when adding <code>{@link #phantom}</code> records to a * {@link Ext.data.Store#writer writer enabled store}.</p> * <br><p>Marking a record <code>{@link #dirty}</code> causes the phantom to * be returned by {@link Ext.data.Store#getModifiedRecords} where it will * have a create action composed for it during {@link Ext.data.Store#save store save} * operations.</p> */ setDirty : function() { var me = this, name; me.dirty = true; me.fields.each(function(field) { if (field.persist) { name = field.name; me.modified[name] = me.get(name); } }, me); }, //<debug> markDirty : function() { if (Ext.isDefined(Ext.global.console)) { Ext.global.console.warn('Ext.data.Model: markDirty has been deprecated. Use setDirty instead.'); } return this.setDirty.apply(this, arguments); }, //</debug> /** * Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. * Rejects all changes made to the model instance since either creation, or the last commit operation. * Modified fields are reverted to their original values. * <p>Developers should subscribe to the {@link Ext.data.Store#update} event * to have their code notified of reject operations.</p> * @param {Boolean} silent (optional) True to skip notification of the owning * store of the change (defaults to false) */ reject : function(silent) { var me = this, modified = me.modified, field; for (field in modified) { if (modified.hasOwnProperty(field)) { if (typeof modified[field] != "function") { me[me.persistanceProperty][field] = modified[field]; } } } me.dirty = false; me.editing = false; me.modified = {}; if (silent !== true) { me.afterReject(); } }, /** * Usually called by the {@link Ext.data.Store} which owns the model instance. * Commits all changes made to the instance since either creation or the last commit operation. * <p>Developers should subscribe to the {@link Ext.data.Store#update} event * to have their code notified of commit operations.</p> * @param {Boolean} silent (optional) True to skip notification of the owning * store of the change (defaults to false) */ commit : function(silent) { var me = this; me.dirty = false; me.editing = false; me.modified = {}; if (silent !== true) { me.afterCommit(); } }, /** * Creates a copy (clone) of this Model instance. * @param {String} id (optional) A new id, defaults to the id * of the instance being copied. See <code>{@link #id}</code>. * To generate a phantom instance with a new id use:<pre><code> var rec = record.copy(); // clone the record Ext.data.Model.id(rec); // automatically generate a unique sequential id * </code></pre> * @return {Record} */ copy : function(newId) { var me = this; return new me.self(Ext.apply({}, me[me.persistanceProperty]), newId || me.internalId); }, /** * Sets the Proxy to use for this model. Accepts any options that can be accepted by {@link Ext#createByAlias Ext.createByAlias} * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy * @static */ setProxy: function(proxy) { //make sure we have an Ext.data.proxy.Proxy object if (!proxy.isProxy) { if (typeof proxy === "string") { proxy = { type: proxy }; } proxy = Ext.createByAlias("proxy." + proxy.type, proxy); } proxy.setModel(this.self); this.proxy = proxy; return proxy; }, /** * Returns the configured Proxy for this Model * @return {Ext.data.proxy.Proxy} The proxy */ getProxy: function() { return this.proxy; }, /** * Validates the current data against all of its configured {@link #validations} and returns an * {@link Ext.data.Errors Errors} object * @return {Ext.data.Errors} The errors object */ validate: function() { var errors = Ext.create('Ext.data.Errors'), validations = this.validations, validators = Ext.data.validations, length, validation, field, valid, type, i; if (validations) { length = validations.length; for (i = 0; i < length; i++) { validation = validations[i]; field = validation.field || validation.name; type = validation.type; valid = validators[type](validation, this.get(field)); if (!valid) { errors.add({ field : field, message: validation.message || validators[type + 'Message'] }); } } } return errors; }, /** * Checks if the model is valid. See {@link #validate}. * @return {Boolean} True if the model is valid. */ isValid: function(){ return this.validate().isValid(); }, /** * Saves the model instance using the configured proxy * @param {Object} options Options to pass to the proxy * @return {Ext.data.Model} The Model instance */ save: function(options) { options = Ext.apply({}, options); var me = this, action = me.phantom ? 'create' : 'update', record = null, scope = options.scope || me, operation, callback; Ext.apply(options, { records: [me], action : action }); operation = Ext.create('Ext.data.Operation', options); callback = function(operation) { if (operation.wasSuccessful()) { record = operation.getRecords()[0]; //we need to make sure we've set the updated data here. Ideally this will be redundant once the //ModelCache is in place me.set(record.data); record.dirty = false; Ext.callback(options.success, scope, [record, operation]); } else { Ext.callback(options.failure, scope, [record, operation]); } Ext.callback(options.callback, scope, [record, operation]); }; me.getProxy()[action](operation, callback, me); return me; }, /** * Destroys the model using the configured proxy * @param {Object} options Options to pass to the proxy * @return {Ext.data.Model} The Model instance */ destroy: function(options){ options = Ext.apply({}, options); var me = this, record = null, scope = options.scope || me, operation, callback; Ext.apply(options, { records: [me], action : 'destroy' }); operation = Ext.create('Ext.data.Operation', options); callback = function(operation) { if (operation.wasSuccessful()) { Ext.callback(options.success, scope, [record, operation]); } else { Ext.callback(options.failure, scope, [record, operation]); } Ext.callback(options.callback, scope, [record, operation]); }; me.getProxy().destroy(operation, callback, me); return me; }, /** * Returns the unique ID allocated to this model instance as defined by {@link #idProperty} * @return {Number} The id */ getId: function() { return this.get(this.idProperty); }, /** * Sets the model instance's id field to the given id * @param {Number} id The new id */ setId: function(id) { this.set(this.idProperty, id); }, /** * Tells this model instance that it has been added to a store * @param {Ext.data.Store} store The store that the model has been added to */ join : function(store) { /** * The {@link Ext.data.Store} to which this Record belongs. * @property store * @type {Ext.data.Store} */ this.store = store; }, /** * Tells this model instance that it has been removed from the store */ unjoin: function() { delete this.store; }, /** * @private * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's * afterEdit method is called */ afterEdit : function() { this.callStore('afterEdit'); }, /** * @private * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's * afterReject method is called */ afterReject : function() { this.callStore("afterReject"); }, /** * @private * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's * afterCommit method is called */ afterCommit: function() { this.callStore('afterCommit'); }, /** * @private * Helper function used by afterEdit, afterReject and afterCommit. Calls the given method on the * {@link Ext.data.Store store} that this instance has {@link #join joined}, if any. The store function * will always be called with the model instance as its single argument. * @param {String} fn The function to call on the store */ callStore: function(fn) { var store = this.store; if (store !== undefined && typeof store[fn] == "function") { store[fn](this); } }, /** * Gets all of the data from this Models *loaded* associations. * It does this recursively - for example if we have a User which * hasMany Orders, and each Order hasMany OrderItems, it will return an object like this: * { * orders: [ * { * id: 123, * status: 'shipped', * orderItems: [ * ... * ] * } * ] * } * @return {Object} The nested data set for the Model's loaded associations */ getAssociatedData: function(){ return this.prepareAssociatedData(this, [], null); }, /** * @private * This complex-looking method takes a given Model instance and returns an object containing all data from * all of that Model's *loaded* associations. See (@link #getAssociatedData} * @param {Ext.data.Model} record The Model instance * @param {Array} ids PRIVATE. The set of Model instance internalIds that have already been loaded * @param {String} associationType (optional) The name of the type of association to limit to. * @return {Object} The nested data set for the Model's loaded associations */ prepareAssociatedData: function(record, ids, associationType) { //we keep track of all of the internalIds of the models that we have loaded so far in here var associations = record.associations.items, associationCount = associations.length, associationData = {}, associatedStore, associatedName, associatedRecords, associatedRecord, associatedRecordCount, association, id, i, j, type, allow; for (i = 0; i < associationCount; i++) { association = associations[i]; type = association.type; allow = true; if (associationType) { allow = type == associationType; } if (allow && type == 'hasMany') { //this is the hasMany store filled with the associated data associatedStore = record[association.storeName]; //we will use this to contain each associated record's data associationData[association.name] = []; //if it's loaded, put it into the association data if (associatedStore && associatedStore.data.length > 0) { associatedRecords = associatedStore.data.items; associatedRecordCount = associatedRecords.length; //now we're finally iterating over the records in the association. We do this recursively for (j = 0; j < associatedRecordCount; j++) { associatedRecord = associatedRecords[j]; // Use the id, since it is prefixed with the model name, guaranteed to be unique id = associatedRecord.id; //when we load the associations for a specific model instance we add it to the set of loaded ids so that //we don't load it twice. If we don't do this, we can fall into endless recursive loading failures. if (Ext.Array.indexOf(ids, id) == -1) { ids.push(id); associationData[association.name][j] = associatedRecord.data; Ext.apply(associationData[association.name][j], this.prepareAssociatedData(associatedRecord, ids, type)); } } } } else if (allow && type == 'belongsTo') { associatedRecord = record[association.instanceName]; if (associatedRecord !== undefined) { id = associatedRecord.id; if (Ext.Array.indexOf(ids, id) == -1) { ids.push(id); associationData[association.name] = associatedRecord.data; Ext.apply(associationData[association.name], this.prepareAssociatedData(associatedRecord, ids, type)); } } } } return associationData; } });