/**
* @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}
*/
constructor: function(data, id) {
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);
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;
}
});