1723 lines
60 KiB
JavaScript
1723 lines
60 KiB
JavaScript
/**
|
|
* @author Ed Spencer
|
|
* @aside guide models
|
|
*
|
|
* 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.data.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.
|
|
*
|
|
* Models are defined as a set of fields and any arbitrary methods and properties relevant to the model. For example:
|
|
*
|
|
* Ext.define('User', {
|
|
* extend: 'Ext.data.Model',
|
|
*
|
|
* config: {
|
|
* 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);
|
|
* }
|
|
* });
|
|
*
|
|
* The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link
|
|
* Ext.data.ModelManager ModelManager}, and all other functions and properties are copied to the new Model's prototype.
|
|
*
|
|
* Now we can create instances of our User model and call any model logic we defined:
|
|
*
|
|
* var user = Ext.create('User', {
|
|
* name : 'Conan',
|
|
* age : 24,
|
|
* phone: '555-555-5555'
|
|
* });
|
|
*
|
|
* user.changeName();
|
|
* user.get('name'); // returns "Conan The Barbarian"
|
|
*
|
|
* # Validations
|
|
*
|
|
* 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:
|
|
*
|
|
* Ext.define('User', {
|
|
* extend: 'Ext.data.Model',
|
|
*
|
|
* config: {
|
|
* 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}/}
|
|
* ]
|
|
* }
|
|
* });
|
|
*
|
|
* The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors}
|
|
* object:
|
|
*
|
|
* var instance = Ext.create('User', {
|
|
* name: 'Ed',
|
|
* gender: 'Male',
|
|
* username: 'edspencer'
|
|
* });
|
|
*
|
|
* var errors = instance.validate();
|
|
*
|
|
* # Associations
|
|
*
|
|
* Models can have associations with other Models via {@link Ext.data.association.HasOne},
|
|
* {@link Ext.data.association.BelongsTo belongsTo} and {@link Ext.data.association.HasMany 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:
|
|
*
|
|
* Ext.define('Post', {
|
|
* extend: 'Ext.data.Model',
|
|
*
|
|
* config: {
|
|
* fields: ['id', 'user_id'],
|
|
* belongsTo: 'User',
|
|
* hasMany : {model: 'Comment', name: 'comments'}
|
|
* }
|
|
* });
|
|
*
|
|
* Ext.define('Comment', {
|
|
* extend: 'Ext.data.Model',
|
|
*
|
|
* config: {
|
|
* fields: ['id', 'user_id', 'post_id'],
|
|
* belongsTo: 'Post'
|
|
* }
|
|
* });
|
|
*
|
|
* Ext.define('User', {
|
|
* extend: 'Ext.data.Model',
|
|
*
|
|
* config: {
|
|
* fields: ['id'],
|
|
* hasMany: [
|
|
* 'Post',
|
|
* {model: 'Comment', name: 'comments'}
|
|
* ]
|
|
* }
|
|
* });
|
|
*
|
|
* See the docs for {@link Ext.data.association.HasOne}, {@link Ext.data.association.BelongsTo} and
|
|
* {@link Ext.data.association.HasMany} for details on the usage and configuration of associations.
|
|
* Note that associations can also be specified like this:
|
|
*
|
|
* Ext.define('User', {
|
|
* extend: 'Ext.data.Model',
|
|
*
|
|
* config: {
|
|
* fields: ['id'],
|
|
* associations: [
|
|
* {type: 'hasMany', model: 'Post', name: 'posts'},
|
|
* {type: 'hasMany', model: 'Comment', name: 'comments'}
|
|
* ]
|
|
* }
|
|
* });
|
|
*
|
|
* # Using a Proxy
|
|
*
|
|
* 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:
|
|
*
|
|
* Ext.define('User', {
|
|
* extend: 'Ext.data.Model',
|
|
*
|
|
* config: {
|
|
* fields: ['id', 'name', 'email'],
|
|
* proxy: {
|
|
* type: 'rest',
|
|
* url : '/users'
|
|
* }
|
|
* }
|
|
* });
|
|
*
|
|
* 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:
|
|
*
|
|
* var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
|
|
*
|
|
* user.save(); //POST /users
|
|
*
|
|
* 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.
|
|
*
|
|
* Loading data via the Proxy is equally easy:
|
|
*
|
|
* //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
|
|
* }
|
|
* });
|
|
*
|
|
* Models can also be updated and destroyed easily:
|
|
*
|
|
* //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.erase({
|
|
* success: function() {
|
|
* console.log('The User was destroyed!');
|
|
* }
|
|
* });
|
|
*
|
|
* # Usage in Stores
|
|
*
|
|
* 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}:
|
|
*
|
|
* var store = Ext.create('Ext.data.Store', {
|
|
* model: 'User'
|
|
* });
|
|
*
|
|
* //uses the Proxy we set up on Model to load the Store data
|
|
* store.load();
|
|
*
|
|
* 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.
|
|
*/
|
|
Ext.define('Ext.data.Model', {
|
|
alternateClassName: 'Ext.data.Record',
|
|
|
|
mixins: {
|
|
observable: 'Ext.mixin.Observable'
|
|
},
|
|
|
|
/**
|
|
* Provides an easy way to quickly determine if a given class is a Model
|
|
* @property isModel
|
|
* @type Boolean
|
|
* @private
|
|
*/
|
|
isModel: true,
|
|
|
|
requires: [
|
|
'Ext.util.Collection',
|
|
'Ext.data.Field',
|
|
'Ext.data.identifier.Simple',
|
|
'Ext.data.ModelManager',
|
|
'Ext.data.proxy.Ajax',
|
|
'Ext.data.association.HasMany',
|
|
'Ext.data.association.BelongsTo',
|
|
'Ext.data.association.HasOne',
|
|
'Ext.data.Errors'
|
|
],
|
|
|
|
config: {
|
|
/**
|
|
* @cfg {String} idProperty
|
|
* The name of the field treated as this Model's unique `id`. Note that this field
|
|
* needs to have a type of 'auto'. Setting the field type to anything else will be undone by the
|
|
* framework. This is because new records that are created without an `id`, will have one generated.
|
|
*/
|
|
idProperty: 'id',
|
|
|
|
data: null,
|
|
|
|
/**
|
|
* @cfg {Object[]/String[]} fields
|
|
* The {@link Ext.data.Model field} definitions for all instances of this Model.
|
|
*
|
|
* __Note:__ this does not set the *values* of each
|
|
* field on an instance, it sets the collection of fields itself.
|
|
*
|
|
* Sample usage:
|
|
*
|
|
* Ext.define('MyApp.model.User', {
|
|
* extend: 'Ext.data.Model',
|
|
*
|
|
* config: {
|
|
* fields: [
|
|
* 'id',
|
|
* {name: 'age', type: 'int'},
|
|
* {name: 'taxRate', type: 'float'}
|
|
* ]
|
|
* }
|
|
* });
|
|
* @accessor
|
|
*/
|
|
fields: undefined,
|
|
|
|
/**
|
|
* @cfg {Object[]} validations
|
|
* An array of {@link Ext.data.Validations validations} for this model.
|
|
*/
|
|
validations: null,
|
|
|
|
/**
|
|
* @cfg {Object[]} associations
|
|
* An array of {@link Ext.data.association.Association associations} for this model.
|
|
*/
|
|
associations: null,
|
|
|
|
/**
|
|
* @cfg {String/Object/String[]/Object[]} hasMany
|
|
* One or more {@link Ext.data.association.HasMany HasMany associations} for this model.
|
|
*/
|
|
hasMany: null,
|
|
|
|
/**
|
|
* @cfg {String/Object/String[]/Object[]} hasOne
|
|
* One or more {@link Ext.data.association.HasOne HasOne associations} for this model.
|
|
*/
|
|
hasOne: null,
|
|
|
|
/**
|
|
* @cfg {String/Object/String[]/Object[]} belongsTo
|
|
* One or more {@link Ext.data.association.BelongsTo BelongsTo associations} for this model.
|
|
*/
|
|
belongsTo: null,
|
|
|
|
/**
|
|
* @cfg {Object/Ext.data.Proxy} proxy
|
|
* The string type of the default Model Proxy.
|
|
* @accessor
|
|
*/
|
|
proxy: null,
|
|
|
|
|
|
/**
|
|
* @cfg {Object/String} identifier
|
|
* The identifier strategy used when creating new instances of this Model that don't have an id defined.
|
|
* By default this uses the simple identifier strategy that generates id's like 'ext-record-12'. If you are
|
|
* saving these records in localstorage using a LocalStorage proxy you need to ensure that this identifier
|
|
* strategy is set to something that always generates unique id's. We provide one strategy by default that
|
|
* generates these unique id's which is the uuid strategy.
|
|
*/
|
|
identifier: {
|
|
type: 'simple'
|
|
},
|
|
|
|
/**
|
|
* @cfg {String} clientIdProperty
|
|
* The name of a property that is used for submitting this Model's unique client-side identifier
|
|
* to the server when multiple phantom records are saved as part of the same {@link Ext.data.Operation Operation}.
|
|
* In such a case, the server response should include the client id for each record
|
|
* so that the server response data can be used to update the client-side records if necessary.
|
|
* This property cannot have the same name as any of this Model's fields.
|
|
* @accessor
|
|
*/
|
|
clientIdProperty: 'clientId',
|
|
|
|
/**
|
|
* @method getIsErased Returns `true` if the record has been erased on the server.
|
|
*/
|
|
isErased: false,
|
|
|
|
/**
|
|
* @cfg {Boolean} useCache
|
|
* Change this to `false` if you want to ensure that new instances are created for each id. For example,
|
|
* this is needed when adding the same tree nodes to multiple trees.
|
|
*/
|
|
useCache: true
|
|
},
|
|
|
|
staticConfigs: [
|
|
'idProperty',
|
|
'fields',
|
|
'validations',
|
|
'associations',
|
|
'hasMany',
|
|
'hasOne',
|
|
'belongsTo',
|
|
'clientIdProperty',
|
|
'identifier',
|
|
'useCache',
|
|
'proxy'
|
|
],
|
|
|
|
statics: {
|
|
EDIT : 'edit',
|
|
REJECT : 'reject',
|
|
COMMIT : 'commit',
|
|
|
|
cache: {},
|
|
|
|
generateProxyMethod: function(name) {
|
|
return function() {
|
|
var prototype = this.prototype;
|
|
return prototype[name].apply(prototype, arguments);
|
|
};
|
|
},
|
|
|
|
generateCacheId: function(record, id) {
|
|
var modelName;
|
|
|
|
if (record && record.isModel) {
|
|
modelName = record.modelName;
|
|
if (id === undefined) {
|
|
id = record.getId();
|
|
}
|
|
} else {
|
|
modelName = record;
|
|
}
|
|
|
|
return modelName.replace(/\./g, '-').toLowerCase() + '-' + id;
|
|
}
|
|
},
|
|
|
|
inheritableStatics: {
|
|
/**
|
|
* Asynchronously loads a model instance by id. Sample usage:
|
|
*
|
|
* 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
|
|
* }
|
|
* });
|
|
*
|
|
* @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
|
|
* @static
|
|
* @inheritable
|
|
*/
|
|
load: function(id, config, scope) {
|
|
var proxy = this.getProxy(),
|
|
idProperty = this.getIdProperty(),
|
|
record = null,
|
|
params = {},
|
|
callback, operation;
|
|
|
|
scope = scope || (config && config.scope) || this;
|
|
if (Ext.isFunction(config)) {
|
|
config = {
|
|
callback: config,
|
|
scope: scope
|
|
};
|
|
}
|
|
|
|
params[idProperty] = id;
|
|
config = Ext.apply({}, config);
|
|
config = Ext.applyIf(config, {
|
|
action: 'read',
|
|
params: params,
|
|
model: this
|
|
});
|
|
|
|
operation = Ext.create('Ext.data.Operation', config);
|
|
|
|
if (!proxy) {
|
|
Ext.Logger.error('You are trying to load a model that doesn\'t have a Proxy specified');
|
|
}
|
|
|
|
callback = function(operation) {
|
|
if (operation.wasSuccessful()) {
|
|
record = operation.getRecords()[0] || null;
|
|
Ext.callback(config.success, scope, [record, operation]);
|
|
} else {
|
|
Ext.callback(config.failure, scope, [record, operation]);
|
|
}
|
|
Ext.callback(config.callback, scope, [record, operation]);
|
|
};
|
|
|
|
proxy.read(operation, callback, this);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @property {Boolean} editing
|
|
* @readonly
|
|
* Internal flag used to track whether or not the model instance is currently being edited.
|
|
*/
|
|
editing : false,
|
|
|
|
/**
|
|
* @property {Boolean} dirty
|
|
* @readonly
|
|
* `true` if this Record has been modified.
|
|
*/
|
|
dirty : false,
|
|
|
|
/**
|
|
* @property {Boolean} phantom
|
|
* `true` 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.
|
|
*/
|
|
phantom : false,
|
|
|
|
/**
|
|
* Creates new Model instance.
|
|
* @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.
|
|
* @param [raw]
|
|
* @param [convertedData]
|
|
*/
|
|
constructor: function(data, id, raw, convertedData) {
|
|
var me = this,
|
|
cached = null,
|
|
useCache = me.getUseCache(),
|
|
idProperty = me.getIdProperty();
|
|
|
|
|
|
/**
|
|
* @property {Object} modified key/value pairs of all fields whose values have changed.
|
|
* The value is the original value for the field.
|
|
*/
|
|
me.modified = {};
|
|
|
|
/**
|
|
* @property {Object} raw The raw data used to create this model if created via a reader.
|
|
*/
|
|
me.raw = raw || data || {};
|
|
|
|
/**
|
|
* @property {Array} stores
|
|
* An array of {@link Ext.data.Store} objects that this record is bound to.
|
|
*/
|
|
me.stores = [];
|
|
|
|
data = data || convertedData || {};
|
|
|
|
// We begin by checking if an id is passed to the constructor. If this is the case we override
|
|
// any possible id value that was passed in the data.
|
|
if (id || id === 0) {
|
|
// Lets skip using set here since it's so much faster
|
|
data[idProperty] = me.internalId = id;
|
|
}
|
|
|
|
id = data[idProperty];
|
|
if (useCache && (id || id === 0)) {
|
|
cached = Ext.data.Model.cache[Ext.data.Model.generateCacheId(this, id)];
|
|
if (cached) {
|
|
return cached.mergeData(convertedData || data || {});
|
|
}
|
|
}
|
|
|
|
if (convertedData) {
|
|
me.setConvertedData(data);
|
|
} else {
|
|
me.setData(data);
|
|
}
|
|
|
|
// If it does not have an id at this point, we generate it using the id strategy. This means
|
|
// that we will treat this record as a phantom record from now on
|
|
id = me.data[idProperty];
|
|
if (!id && id !== 0) {
|
|
me.data[idProperty] = me.internalId = me.id = me.getIdentifier().generate(me);
|
|
me.phantom = true;
|
|
|
|
if (this.associations.length) {
|
|
this.handleInlineAssociationData(data);
|
|
}
|
|
} else {
|
|
me.id = me.getIdentifier().generate(me);
|
|
}
|
|
|
|
if (useCache) {
|
|
Ext.data.Model.cache[Ext.data.Model.generateCacheId(me)] = me;
|
|
}
|
|
|
|
if (this.init && typeof this.init == 'function') {
|
|
this.init();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Private function that is used when you create a record that already exists in the model cache.
|
|
* In this case we loop over each field, and apply any data to the current instance that is not already
|
|
* marked as being dirty on that instance.
|
|
* @param data
|
|
* @return {Ext.data.Model} This record.
|
|
* @private
|
|
*/
|
|
mergeData: function(rawData) {
|
|
var me = this,
|
|
fields = me.getFields().items,
|
|
ln = fields.length,
|
|
modified = me.modified,
|
|
data = me.data,
|
|
i, field, fieldName, value, id;
|
|
|
|
for (i = 0; i < ln; i++) {
|
|
field = fields[i];
|
|
fieldName = field._name;
|
|
value = rawData[fieldName];
|
|
|
|
if (value !== undefined && !modified.hasOwnProperty(fieldName)) {
|
|
if (field._convert) {
|
|
value = field._convert(value, me);
|
|
}
|
|
|
|
data[fieldName] = value;
|
|
}
|
|
}
|
|
|
|
if (me.associations.length) {
|
|
me.handleInlineAssociationData(rawData);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* This method is used to set the data for this Record instance.
|
|
* Note that the existing data is removed. If a field is not specified
|
|
* in the passed data it will use the field's default value. If a convert
|
|
* method is specified for the field it will be called on the value.
|
|
* @param rawData
|
|
* @return {Ext.data.Model} This record.
|
|
*/
|
|
setData: function(rawData) {
|
|
var me = this,
|
|
fields = me.fields.items,
|
|
ln = fields.length,
|
|
isArray = Ext.isArray(rawData),
|
|
data = me._data = me.data = {},
|
|
i, field, name, value, convert, id;
|
|
|
|
if (!rawData) {
|
|
return me;
|
|
}
|
|
|
|
for (i = 0; i < ln; i++) {
|
|
field = fields[i];
|
|
name = field._name;
|
|
convert = field._convert;
|
|
|
|
if (isArray) {
|
|
value = rawData[i];
|
|
}
|
|
else {
|
|
value = rawData[name];
|
|
if (typeof value == 'undefined') {
|
|
value = field._defaultValue;
|
|
}
|
|
}
|
|
|
|
if (convert) {
|
|
value = field._convert(value, me);
|
|
}
|
|
|
|
data[name] = value;
|
|
}
|
|
|
|
id = me.getId();
|
|
if (me.associations.length && (id || id === 0)) {
|
|
me.handleInlineAssociationData(rawData);
|
|
}
|
|
|
|
return me;
|
|
},
|
|
|
|
handleInlineAssociationData: function(data) {
|
|
var associations = this.associations.items,
|
|
ln = associations.length,
|
|
i, association, associationData, reader, proxy, associationKey;
|
|
|
|
for (i = 0; i < ln; i++) {
|
|
association = associations[i];
|
|
associationKey = association.getAssociationKey();
|
|
associationData = data[associationKey];
|
|
|
|
if (associationData) {
|
|
reader = association.getReader();
|
|
if (!reader) {
|
|
proxy = association.getAssociatedModel().getProxy();
|
|
// if the associated model has a Reader already, use that, otherwise attempt to create a sensible one
|
|
if (proxy) {
|
|
reader = proxy.getReader();
|
|
} else {
|
|
reader = new Ext.data.JsonReader({
|
|
model: association.getAssociatedModel()
|
|
});
|
|
}
|
|
}
|
|
association.read(this, reader, associationData);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets the model instance's id field to the given id.
|
|
* @param {Number/String} id The new id
|
|
*/
|
|
setId: function(id) {
|
|
var currentId = this.getId();
|
|
|
|
// Lets use the direct property instead of getter here
|
|
this.set(this.getIdProperty(), id);
|
|
|
|
// We don't update the this.id since we don't want to break listeners that already
|
|
// exist on the record instance.
|
|
this.internalId = id;
|
|
|
|
if (this.getUseCache()) {
|
|
delete Ext.data.Model.cache[Ext.data.Model.generateCacheId(this, currentId)];
|
|
Ext.data.Model.cache[Ext.data.Model.generateCacheId(this)] = this;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the unique ID allocated to this model instance as defined by {@link #idProperty}.
|
|
* @return {Number/String} The `id`.
|
|
*/
|
|
getId: function() {
|
|
// Lets use the direct property instead of getter here
|
|
return this.get(this.getIdProperty());
|
|
},
|
|
|
|
/**
|
|
* This sets the data directly without converting and applying default values.
|
|
* This method is used when a Record gets instantiated by a Reader. Only use
|
|
* this when you are sure you are passing correctly converted data.
|
|
* @param data
|
|
* @return {Ext.data.Model} This Record.
|
|
*/
|
|
setConvertedData: function(data) {
|
|
this._data = this.data = data;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns the value of the given field.
|
|
* @param {String} fieldName The field to fetch the value for.
|
|
* @return {Object} The value.
|
|
*/
|
|
get: function(fieldName) {
|
|
return this.data[fieldName];
|
|
},
|
|
|
|
/**
|
|
* 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 {Object} value The value to set.
|
|
*/
|
|
set: function(fieldName, value) {
|
|
var me = this,
|
|
// We are using the fields map since it saves lots of function calls
|
|
fieldMap = me.fields.map,
|
|
modified = me.modified,
|
|
notEditing = !me.editing,
|
|
modifiedCount = 0,
|
|
modifiedFieldNames = [],
|
|
field, key, i, currentValue, ln, convert;
|
|
|
|
/*
|
|
* 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) {
|
|
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 = fieldMap[key];
|
|
if (field && field.hasCustomConvert()) {
|
|
modifiedFieldNames.push(key);
|
|
continue;
|
|
}
|
|
|
|
if (!modifiedCount && notEditing) {
|
|
me.beginEdit();
|
|
}
|
|
++modifiedCount;
|
|
me.set(key, fieldName[key]);
|
|
}
|
|
}
|
|
|
|
ln = modifiedFieldNames.length;
|
|
if (ln) {
|
|
if (!modifiedCount && notEditing) {
|
|
me.beginEdit();
|
|
}
|
|
modifiedCount += ln;
|
|
for (i = 0; i < ln; i++) {
|
|
field = modifiedFieldNames[i];
|
|
me.set(field, fieldName[field]);
|
|
}
|
|
}
|
|
|
|
if (notEditing && modifiedCount) {
|
|
me.endEdit(false, modifiedFieldNames);
|
|
}
|
|
} else {
|
|
field = fieldMap[fieldName];
|
|
convert = field && field.getConvert();
|
|
if (convert) {
|
|
value = convert.call(field, value, me);
|
|
}
|
|
|
|
currentValue = me.data[fieldName];
|
|
me.data[fieldName] = value;
|
|
|
|
if (field && !me.isEqual(currentValue, value)) {
|
|
if (modified.hasOwnProperty(fieldName)) {
|
|
if (me.isEqual(modified[fieldName], value)) {
|
|
// the original value in me.modified equals the new value, so the
|
|
// field is no longer modified
|
|
delete modified[fieldName];
|
|
// we might have removed the last modified field, so check to see if
|
|
// there are any modified fields remaining and correct me.dirty:
|
|
me.dirty = false;
|
|
for (key in modified) {
|
|
if (modified.hasOwnProperty(key)) {
|
|
me.dirty = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
me.dirty = true;
|
|
// We only go one level back?
|
|
modified[fieldName] = currentValue;
|
|
}
|
|
}
|
|
|
|
if (notEditing) {
|
|
me.afterEdit([fieldName], modified);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* Begins an edit. While in edit mode, no events (e.g. the `update` 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;
|
|
|
|
// We save the current states of dirty, data and modified so that when we
|
|
// cancel the edit, we can put it back to this state
|
|
me.dirtySave = me.dirty;
|
|
me.dataSave = Ext.apply({}, me.data);
|
|
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.data = me.dataSave;
|
|
me.dirty = me.dirtySave;
|
|
|
|
// Delete the saved states
|
|
delete me.modifiedSave;
|
|
delete me.dataSave;
|
|
delete me.dirtySave;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ends an edit. If any data was modified, the containing store is notified (ie, the store's `update` event will
|
|
* fire).
|
|
* @param {Boolean} silent `true` to not notify the store of the change.
|
|
* @param {String[]} modifiedFieldNames Array of field names changed during edit.
|
|
*/
|
|
endEdit: function(silent, modifiedFieldNames) {
|
|
var me = this;
|
|
|
|
if (me.editing) {
|
|
me.editing = false;
|
|
|
|
if (silent !== true && (me.changedWhileEditing())) {
|
|
me.afterEdit(modifiedFieldNames || Ext.Object.getKeys(this.modified), this.modified);
|
|
}
|
|
|
|
delete me.modifiedSave;
|
|
delete me.dataSave;
|
|
delete me.dirtySave;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Checks if the underlying data has changed during an edit. This doesn't necessarily
|
|
* mean the record is dirty, however we still need to notify the store since it may need
|
|
* to update any views.
|
|
* @private
|
|
* @return {Boolean} `true` if the underlying data has changed during an edit.
|
|
*/
|
|
changedWhileEditing: function() {
|
|
var me = this,
|
|
saved = me.dataSave,
|
|
data = me.data,
|
|
key;
|
|
|
|
for (key in data) {
|
|
if (data.hasOwnProperty(key)) {
|
|
if (!me.isEqual(data[key], saved[key])) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Gets a hash of only the fields that have been modified since this Model was created or committed.
|
|
* @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 `true` if the passed field name has been `{@link #modified}` since the load or last commit.
|
|
* @param {String} fieldName {@link Ext.data.Field#name}
|
|
* @return {Boolean}
|
|
*/
|
|
isModified : function(fieldName) {
|
|
return this.modified.hasOwnProperty(fieldName);
|
|
},
|
|
|
|
/**
|
|
* Saves the model instance using the configured proxy.
|
|
*
|
|
* @param {Object/Function} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
|
|
* If you pass a function, this will automatically become the callback method. For convenience the config
|
|
* object may also contain `success` and `failure` methods in addition to `callback` - they will all be invoked
|
|
* with the Model and Operation as arguments.
|
|
* @param {Object} scope The scope to run your callback method in. This is only used if you passed a function
|
|
* as the first argument.
|
|
* @return {Ext.data.Model} The Model instance
|
|
*/
|
|
save: function(options, scope) {
|
|
var me = this,
|
|
action = me.phantom ? 'create' : 'update',
|
|
proxy = me.getProxy(),
|
|
operation,
|
|
callback;
|
|
|
|
if (!proxy) {
|
|
Ext.Logger.error('You are trying to save a model instance that doesn\'t have a Proxy specified');
|
|
}
|
|
|
|
options = options || {};
|
|
scope = scope || me;
|
|
|
|
if (Ext.isFunction(options)) {
|
|
options = {
|
|
callback: options,
|
|
scope: scope
|
|
};
|
|
}
|
|
|
|
Ext.applyIf(options, {
|
|
records: [me],
|
|
action : action,
|
|
model: me.self
|
|
});
|
|
|
|
operation = Ext.create('Ext.data.Operation', options);
|
|
|
|
callback = function(operation) {
|
|
if (operation.wasSuccessful()) {
|
|
Ext.callback(options.success, scope, [me, operation]);
|
|
} else {
|
|
Ext.callback(options.failure, scope, [me, operation]);
|
|
}
|
|
|
|
Ext.callback(options.callback, scope, [me, operation]);
|
|
};
|
|
|
|
proxy[action](operation, callback, me);
|
|
|
|
return me;
|
|
},
|
|
|
|
/**
|
|
* Destroys the record using the configured proxy. This will create a 'destroy' operation.
|
|
* Note that this doesn't destroy this instance after the server comes back with a response.
|
|
* It will however call `afterErase` on any Stores it is joined to. Stores by default will
|
|
* automatically remove this instance from their data collection.
|
|
*
|
|
* @param {Object/Function} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
|
|
* If you pass a function, this will automatically become the callback method. For convenience the config
|
|
* object may also contain `success` and `failure` methods in addition to `callback` - they will all be invoked
|
|
* with the Model and Operation as arguments.
|
|
* @param {Object} scope The scope to run your callback method in. This is only used if you passed a function
|
|
* as the first argument.
|
|
* @return {Ext.data.Model} The Model instance.
|
|
*/
|
|
erase: function(options, scope) {
|
|
var me = this,
|
|
proxy = this.getProxy(),
|
|
operation,
|
|
callback;
|
|
|
|
if (!proxy) {
|
|
Ext.Logger.error('You are trying to erase a model instance that doesn\'t have a Proxy specified');
|
|
}
|
|
|
|
options = options || {};
|
|
scope = scope || me;
|
|
|
|
if (Ext.isFunction(options)) {
|
|
options = {
|
|
callback: options,
|
|
scope: scope
|
|
};
|
|
}
|
|
|
|
Ext.applyIf(options, {
|
|
records: [me],
|
|
action : 'destroy',
|
|
model: this.self
|
|
});
|
|
|
|
operation = Ext.create('Ext.data.Operation', options);
|
|
|
|
callback = function(operation) {
|
|
if (operation.wasSuccessful()) {
|
|
Ext.callback(options.success, scope, [me, operation]);
|
|
} else {
|
|
Ext.callback(options.failure, scope, [me, operation]);
|
|
}
|
|
|
|
Ext.callback(options.callback, scope, [me, operation]);
|
|
};
|
|
|
|
proxy.destroy(operation, callback, me);
|
|
|
|
return me;
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of reject
|
|
* operations.
|
|
*
|
|
* @param {Boolean} [silent=false] (optional) `true` to skip notification of the owning store of the change.
|
|
*/
|
|
reject: function(silent) {
|
|
var me = this,
|
|
modified = me.modified,
|
|
field;
|
|
|
|
for (field in modified) {
|
|
if (modified.hasOwnProperty(field)) {
|
|
if (typeof modified[field] != "function") {
|
|
me.data[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.
|
|
*
|
|
* Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of commit
|
|
* operations.
|
|
*
|
|
* @param {Boolean} [silent=false] (optional) `true` to skip notification of the owning store of the change.
|
|
*/
|
|
commit: function(silent) {
|
|
var me = this,
|
|
modified = this.modified;
|
|
|
|
me.phantom = me.dirty = me.editing = false;
|
|
me.modified = {};
|
|
|
|
if (silent !== true) {
|
|
me.afterCommit(modified);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
|
|
* `afterEdit` method is called.
|
|
* @param {String[]} modifiedFieldNames Array of field names changed during edit.
|
|
*/
|
|
afterEdit : function(modifiedFieldNames, modified) {
|
|
this.notifyStores('afterEdit', modifiedFieldNames, modified);
|
|
},
|
|
|
|
/**
|
|
* @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.notifyStores("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(modified) {
|
|
this.notifyStores('afterCommit', Ext.Object.getKeys(modified || {}), modified);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* Helper function used by {@link #afterEdit}, {@link #afterReject}, and {@link #afterCommit}. Calls the given method on the
|
|
* {@link Ext.data.Store store} that this instance has {@link #join join}ed, 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.
|
|
*/
|
|
notifyStores: function(fn) {
|
|
var args = Ext.Array.clone(arguments),
|
|
stores = this.stores,
|
|
ln = stores.length,
|
|
i, store;
|
|
|
|
args[0] = this;
|
|
for (i = 0; i < ln; ++i) {
|
|
store = stores[i];
|
|
if (store !== undefined && typeof store[fn] == "function") {
|
|
store[fn].apply(store, args);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Creates a copy (clone) of this Model instance.
|
|
*
|
|
* @param {String} id A new `id`. If you don't specify this a new `id` will be generated for you.
|
|
* To generate a phantom instance with a new `id` use:
|
|
*
|
|
* var rec = record.copy(); // clone the record with a new id
|
|
*
|
|
* @return {Ext.data.Model}
|
|
*/
|
|
copy: function(newId) {
|
|
var me = this,
|
|
idProperty = me.getIdProperty(),
|
|
raw = Ext.apply({}, me.raw),
|
|
data = Ext.apply({}, me.data);
|
|
|
|
delete raw[idProperty];
|
|
delete data[idProperty];
|
|
|
|
return new me.self(null, newId, raw, data);
|
|
},
|
|
|
|
/**
|
|
* Returns an object containing the data set on this record. This method also allows you to
|
|
* retrieve all the associated data. Note that if you should always use this method if you
|
|
* need all the associated data, since the data property on the record instance is not
|
|
* ensured to be updated at all times.
|
|
* @param {Boolean} includeAssociated `true` to include the associated data.
|
|
* @return {Object} The data.
|
|
*/
|
|
getData: function(includeAssociated) {
|
|
var data = this.data;
|
|
|
|
if (includeAssociated === true) {
|
|
Ext.apply(data, this.getAssociatedData());
|
|
}
|
|
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* 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 {String[]} 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, associationName, associatedRecords, associatedRecord,
|
|
associatedRecordCount, association, id, i, j, type, allow;
|
|
|
|
for (i = 0; i < associationCount; i++) {
|
|
association = associations[i];
|
|
associationName = association.getName();
|
|
type = association.getType();
|
|
allow = true;
|
|
|
|
if (associationType) {
|
|
allow = type == associationType;
|
|
}
|
|
|
|
if (allow && type.toLowerCase() == 'hasmany') {
|
|
//this is the hasMany store filled with the associated data
|
|
associatedStore = record[association.getStoreName()];
|
|
|
|
//we will use this to contain each associated record's data
|
|
associationData[associationName] = [];
|
|
|
|
//if it's loaded, put it into the association data
|
|
if (associatedStore && associatedStore.getCount() > 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[associationName][j] = associatedRecord.getData();
|
|
Ext.apply(associationData[associationName][j], this.prepareAssociatedData(associatedRecord, ids, associationType));
|
|
}
|
|
}
|
|
}
|
|
} else if (allow && (type.toLowerCase() == 'belongsto' || type.toLowerCase() == 'hasone')) {
|
|
associatedRecord = record[association.getInstanceName()];
|
|
if (associatedRecord !== undefined) {
|
|
id = associatedRecord.id;
|
|
if (Ext.Array.indexOf(ids, id) === -1) {
|
|
ids.push(id);
|
|
associationData[associationName] = associatedRecord.getData();
|
|
Ext.apply(associationData[associationName], this.prepareAssociatedData(associatedRecord, ids, associationType));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return associationData;
|
|
},
|
|
|
|
/**
|
|
* By joining this model to an instance of a class, this model will automatically try to
|
|
* call certain template methods on that instance ({@link #afterEdit}, {@link #afterCommit}, {@link Ext.data.Store#afterErase}).
|
|
* For example, a Store calls join and unjoin whenever you add or remove a record to it's data collection.
|
|
* This way a Store can get notified of any changes made to this record.
|
|
* This functionality is usually only required when creating custom components.
|
|
* @param {Ext.data.Store} store The store to which this model has been added.
|
|
*/
|
|
join: function(store) {
|
|
Ext.Array.include(this.stores, store);
|
|
},
|
|
|
|
/**
|
|
* This un-joins this record from an instance of a class. Look at the documentation for {@link #join}
|
|
* for more information about joining records to class instances.
|
|
* @param {Ext.data.Store} store The store from which this model has been removed.
|
|
*/
|
|
unjoin: function(store) {
|
|
Ext.Array.remove(this.stores, store);
|
|
},
|
|
|
|
/**
|
|
* Marks this **Record** as `{@link #dirty}`. This method is used internally when adding `{@link #phantom}` records
|
|
* to a {@link Ext.data.proxy.Server#writer writer enabled store}.
|
|
*
|
|
* Marking a record `{@link #dirty}` causes the phantom to be returned by {@link Ext.data.Store#getUpdatedRecords}
|
|
* where it will have a create action composed for it during {@link Ext.data.Model#save model save} operations.
|
|
*/
|
|
setDirty : function() {
|
|
var me = this,
|
|
name;
|
|
|
|
me.dirty = true;
|
|
|
|
me.fields.each(function(field) {
|
|
if (field.getPersist()) {
|
|
name = field.getName();
|
|
me.modified[name] = me.get(name);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Validates the current data against all of its configured {@link #cfg-validations}.
|
|
* @return {Ext.data.Errors} The errors object.
|
|
*/
|
|
validate: function() {
|
|
var errors = Ext.create('Ext.data.Errors'),
|
|
validations = this.getValidations().items,
|
|
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(Ext.create('Ext.data.Error', {
|
|
field : field,
|
|
message: validation.message || validators.getMessage(type)
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
},
|
|
|
|
/**
|
|
* Returns a url-suitable string for this model instance. By default this just returns the name of the Model class
|
|
* followed by the instance ID - for example an instance of MyApp.model.User with ID 123 will return 'user/123'.
|
|
* @return {String} The url string for this model instance.
|
|
*/
|
|
toUrl: function() {
|
|
var pieces = this.$className.split('.'),
|
|
name = pieces[pieces.length - 1].toLowerCase();
|
|
|
|
return name + '/' + this.getId();
|
|
},
|
|
|
|
/**
|
|
* Destroys this model instance. Note that this doesn't do a 'destroy' operation. If you want to destroy
|
|
* the record in your localStorage or on the server you should use the {@link #erase} method.
|
|
*/
|
|
destroy: function() {
|
|
var me = this;
|
|
me.notifyStores('afterErase', me);
|
|
if (me.getUseCache()) {
|
|
delete Ext.data.Model.cache[Ext.data.Model.generateCacheId(me)];
|
|
}
|
|
me.raw = me.stores = me.modified = null;
|
|
me.callParent(arguments);
|
|
},
|
|
|
|
//<debug>
|
|
markDirty : function() {
|
|
if (Ext.isDefined(Ext.Logger)) {
|
|
Ext.Logger.deprecate('Ext.data.Model: markDirty has been deprecated. Use setDirty instead.');
|
|
}
|
|
return this.setDirty.apply(this, arguments);
|
|
},
|
|
//</debug>
|
|
|
|
applyProxy: function(proxy, currentProxy) {
|
|
return Ext.factory(proxy, Ext.data.Proxy, currentProxy, 'proxy');
|
|
},
|
|
|
|
updateProxy: function(proxy) {
|
|
if (proxy) {
|
|
proxy.setModel(this.self);
|
|
}
|
|
},
|
|
|
|
applyAssociations: function(associations) {
|
|
if (associations) {
|
|
this.addAssociations(associations, 'hasMany');
|
|
}
|
|
},
|
|
|
|
applyBelongsTo: function(belongsTo) {
|
|
if (belongsTo) {
|
|
this.addAssociations(belongsTo, 'belongsTo');
|
|
}
|
|
},
|
|
|
|
applyHasMany: function(hasMany) {
|
|
if (hasMany) {
|
|
this.addAssociations(hasMany, 'hasMany');
|
|
}
|
|
},
|
|
|
|
applyHasOne: function(hasOne) {
|
|
if (hasOne) {
|
|
this.addAssociations(hasOne, 'hasOne');
|
|
}
|
|
},
|
|
|
|
addAssociations: function(associations, defaultType) {
|
|
var ln, i, association,
|
|
name = this.self.modelName,
|
|
associationsCollection = this.self.associations,
|
|
onCreatedFn;
|
|
|
|
associations = Ext.Array.from(associations);
|
|
|
|
for (i = 0, ln = associations.length; i < ln; i++) {
|
|
association = associations[i];
|
|
if (!Ext.isObject(association)) {
|
|
association = {model: association};
|
|
}
|
|
|
|
Ext.applyIf(association, {
|
|
type: defaultType,
|
|
ownerModel: name,
|
|
associatedModel: association.model
|
|
});
|
|
|
|
delete association.model;
|
|
|
|
onCreatedFn = Ext.Function.bind(function(associationName) {
|
|
associationsCollection.add(Ext.data.association.Association.create(this));
|
|
}, association);
|
|
|
|
Ext.ClassManager.onCreated(onCreatedFn, this, (typeof association.associatedModel === 'string') ? association.associatedModel : Ext.getClassName(association.associatedModel));
|
|
}
|
|
},
|
|
|
|
applyValidations: function(validations) {
|
|
if (validations) {
|
|
if (!Ext.isArray(validations)) {
|
|
validations = [validations];
|
|
}
|
|
this.addValidations(validations);
|
|
}
|
|
},
|
|
|
|
addValidations: function(validations) {
|
|
this.self.validations.addAll(validations);
|
|
},
|
|
|
|
/**
|
|
* @method setFields
|
|
* Updates the collection of Fields that all instances of this Model use. **Does not** update field values in a Model
|
|
* instance (use {@link #set} for that), instead this updates which fields are available on the Model class. This
|
|
* is normally used when creating or updating Model definitions dynamically, for example if you allow your users to
|
|
* define their own Models and save the fields configuration to a database, this method allows you to change those
|
|
* fields later.
|
|
* @return {Array}
|
|
*/
|
|
|
|
applyFields: function(fields) {
|
|
var superFields = this.superclass.fields;
|
|
if (superFields) {
|
|
fields = superFields.items.concat(fields || []);
|
|
}
|
|
return fields || [];
|
|
},
|
|
|
|
updateFields: function(fields) {
|
|
var ln = fields.length,
|
|
me = this,
|
|
prototype = me.self.prototype,
|
|
idProperty = this.getIdProperty(),
|
|
idField, fieldsCollection, field, i;
|
|
|
|
/**
|
|
* @property {Ext.util.MixedCollection} fields
|
|
* The fields defined on this model.
|
|
*/
|
|
fieldsCollection = me._fields = me.fields = new Ext.util.Collection(prototype.getFieldName);
|
|
|
|
for (i = 0; i < ln; i++) {
|
|
field = fields[i];
|
|
if (!field.isField) {
|
|
field = new Ext.data.Field(fields[i]);
|
|
}
|
|
fieldsCollection.add(field);
|
|
}
|
|
|
|
// We want every Model to have an id property field
|
|
idField = fieldsCollection.get(idProperty);
|
|
if (!idField) {
|
|
fieldsCollection.add(new Ext.data.Field(idProperty));
|
|
} else {
|
|
idField.setType('auto');
|
|
}
|
|
|
|
fieldsCollection.addSorter(prototype.sortConvertFields);
|
|
},
|
|
|
|
applyIdentifier: function(identifier) {
|
|
if (typeof identifier === 'string') {
|
|
identifier = {
|
|
type: identifier
|
|
};
|
|
}
|
|
return Ext.factory(identifier, Ext.data.identifier.Simple, this.getIdentifier(), 'data.identifier');
|
|
},
|
|
|
|
/**
|
|
* This method is used by the fields collection to retrieve the key for a field
|
|
* based on it's name.
|
|
* @param field
|
|
* @return {String}
|
|
* @private
|
|
*/
|
|
getFieldName: function(field) {
|
|
return field.getName();
|
|
},
|
|
|
|
/**
|
|
* This method is being used to sort the fields based on their convert method. If
|
|
* a field has a custom convert method, we ensure its more to the bottom of the collection.
|
|
* @param field1
|
|
* @param field2
|
|
* @return {Number}
|
|
* @private
|
|
*/
|
|
sortConvertFields: function(field1, field2) {
|
|
var f1SpecialConvert = field1.hasCustomConvert(),
|
|
f2SpecialConvert = field2.hasCustomConvert();
|
|
|
|
if (f1SpecialConvert && !f2SpecialConvert) {
|
|
return 1;
|
|
}
|
|
if (!f1SpecialConvert && f2SpecialConvert) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
onClassExtended: function(cls, data, hooks) {
|
|
var onBeforeClassCreated = hooks.onBeforeCreated,
|
|
Model = this,
|
|
prototype = Model.prototype,
|
|
configNameCache = Ext.Class.configNameCache,
|
|
staticConfigs = prototype.staticConfigs.concat(data.staticConfigs || []),
|
|
defaultConfig = prototype.config,
|
|
config = data.config || {},
|
|
key;
|
|
|
|
// Convert old properties in data into a config object
|
|
// <deprecated product=touch since=2.0>
|
|
if (data.idgen || config.idgen) {
|
|
config.identifier = data.idgen || config.idgen;
|
|
// <debug warn>
|
|
Ext.Logger.deprecate('idgen is deprecated as a property. Please put it inside the config object' +
|
|
' under the new "identifier" configuration');
|
|
// </debug>
|
|
}
|
|
|
|
for (key in defaultConfig) {
|
|
if (key in data) {
|
|
config[key] = data[key];
|
|
delete data[key];
|
|
// <debug warn>
|
|
Ext.Logger.deprecate(key + ' is deprecated as a property directly on the Model prototype. ' +
|
|
'Please put it inside the config object.');
|
|
// </debug>
|
|
}
|
|
}
|
|
// </deprecated>
|
|
data.config = config;
|
|
|
|
hooks.onBeforeCreated = function(cls, data) {
|
|
var dependencies = [],
|
|
prototype = cls.prototype,
|
|
statics = {},
|
|
config = prototype.config,
|
|
staticConfigsLn = staticConfigs.length,
|
|
copyMethods = ['set', 'get'],
|
|
copyMethodsLn = copyMethods.length,
|
|
associations = config.associations || [],
|
|
name = Ext.getClassName(cls),
|
|
key, methodName, i, j, ln;
|
|
|
|
// Create static setters and getters for each config option
|
|
for (i = 0; i < staticConfigsLn; i++) {
|
|
key = staticConfigs[i];
|
|
|
|
for (j = 0; j < copyMethodsLn; j++) {
|
|
methodName = configNameCache[key][copyMethods[j]];
|
|
if (methodName in prototype) {
|
|
statics[methodName] = Model.generateProxyMethod(methodName);
|
|
}
|
|
}
|
|
}
|
|
|
|
cls.addStatics(statics);
|
|
|
|
// Save modelName on class and its prototype
|
|
cls.modelName = name;
|
|
prototype.modelName = name;
|
|
|
|
// Take out dependencies on other associations and the proxy type
|
|
if (config.belongsTo) {
|
|
dependencies.push('association.belongsto');
|
|
}
|
|
if (config.hasMany) {
|
|
dependencies.push('association.hasmany');
|
|
}
|
|
if (config.hasOne) {
|
|
dependencies.push('association.hasone');
|
|
}
|
|
|
|
for (i = 0,ln = associations.length; i < ln; ++i) {
|
|
dependencies.push('association.' + associations[i].type.toLowerCase());
|
|
}
|
|
|
|
if (config.identifier) {
|
|
if (typeof config.identifier === 'string') {
|
|
dependencies.push('data.identifier.' + config.identifier);
|
|
}
|
|
else if (typeof config.identifier.type === 'string') {
|
|
dependencies.push('data.identifier.' + config.identifier.type);
|
|
}
|
|
}
|
|
|
|
if (config.proxy) {
|
|
if (typeof config.proxy === 'string') {
|
|
dependencies.push('proxy.' + config.proxy);
|
|
}
|
|
else if (typeof config.proxy.type === 'string') {
|
|
dependencies.push('proxy.' + config.proxy.type);
|
|
}
|
|
}
|
|
|
|
if (config.validations) {
|
|
dependencies.push('Ext.data.Validations');
|
|
}
|
|
|
|
Ext.require(dependencies, function() {
|
|
Ext.Function.interceptBefore(hooks, 'onCreated', function() {
|
|
Ext.data.ModelManager.registerType(name, cls);
|
|
|
|
var superCls = cls.prototype.superclass;
|
|
|
|
/**
|
|
* @property {Ext.util.Collection} associations
|
|
* The associations defined on this model.
|
|
*/
|
|
cls.prototype.associations = cls.associations = cls.prototype._associations = (superCls && superCls.associations)
|
|
? superCls.associations.clone()
|
|
: new Ext.util.Collection(function(association) {
|
|
return association.getName();
|
|
});
|
|
|
|
/**
|
|
* @property {Ext.util.Collection} validations
|
|
* The validations defined on this model.
|
|
*/
|
|
cls.prototype.validations = cls.validations = cls.prototype._validations = (superCls && superCls.validations)
|
|
? superCls.validations.clone()
|
|
: new Ext.util.Collection(function(validation) {
|
|
return validation.field ? (validation.field + '-' + validation.type) : (validation.name + '-' + validation.type);
|
|
});
|
|
|
|
cls.prototype = Ext.Object.chain(cls.prototype);
|
|
cls.prototype.initConfig.call(cls.prototype, config);
|
|
|
|
delete cls.prototype.initConfig;
|
|
});
|
|
|
|
onBeforeClassCreated.call(Model, cls, data, hooks);
|
|
});
|
|
};
|
|
}
|
|
});
|