extjs-gpl
Version:
GPL licensed version of Sencha Ext JS
1,372 lines (1,258 loc) • 130 kB
JavaScript
/**
* A Model or Entity represents some object that your application manages. For example, one
* might define a Model for Users, Products, Cars, or other real-world object that we want
* to model in the system. Models are used by {@link Ext.data.Store stores}, which are in
* turn used by many of the data-bound components in Ext.
*
* # Fields
*
* 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',
* fields: [
* {name: 'name', type: 'string'},
* {name: 'age', type: 'int', convert: null},
* {name: 'phone', type: 'string'},
* {name: 'alive', type: 'boolean', defaultValue: true, convert: null}
* ],
*
* changeName: function() {
* var oldName = this.get('name'),
* newName = oldName + " The Barbarian";
*
* this.set('name', newName);
* }
* });
*
* Now we can create instances of our User model and call any model logic we defined:
*
* var user = Ext.create('User', {
* id : 'ABCD12345',
* name : 'Conan',
* age : 24,
* phone: '555-555-5555'
* });
*
* user.changeName();
* user.get('name'); //returns "Conan The Barbarian"
*
* By default, the built in field types such as number and boolean coerce string values
* in the raw data by virtue of their {@link Ext.data.field.Field#method-convert} method.
* When the server can be relied upon to send data in a format that does not need to be
* converted, disabling this can improve performance. The {@link Ext.data.reader.Json Json}
* and {@link Ext.data.reader.Array Array} readers are likely candidates for this
* optimization. To disable field conversions you simply specify `null` for the field's
* {@link Ext.data.field.Field#cfg-convert convert config}.
*
* ## The "id" Field and `idProperty`
*
* A Model definition always has an *identifying field* which should yield a unique key
* for each instance. By default, a field named "id" will be created with a
* {@link Ext.data.Field#mapping mapping} of "id". This happens because of the default
* {@link #idProperty} provided in Model definitions.
*
* To alter which field is the identifying field, use the {@link #idProperty} config.
*
* # Validators
*
* Models have built-in support for field validators. Validators are added to models as in
* the follow example:
*
* 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 }
* ],
*
* validators: {
* age: 'presence',
* name: { type: 'length', min: 2 },
* gender: { type: 'inclusion', list: ['Male', 'Female'] },
* username: [
* { type: 'exclusion', list: ['Admin', 'Operator'] },
* { type: 'format', matcher: /([a-z]+)[0-9]{2,3}/i }
* ]
* }
* });
*
* The derived type of `Ext.data.field.Field` can also provide validation. If `validators`
* need to be duplicated on multiple fields, instead consider creating a custom field type.
*
* ## Validation
*
* The results of the validators can be retrieved via the "associated" validation record:
*
* var instance = Ext.create('User', {
* name: 'Ed',
* gender: 'Male',
* username: 'edspencer'
* });
*
* var validation = instance.getValidation();
*
* The returned object is an instance of `Ext.data.Validation` and has as its fields the
* result of the field `validators`. The validation object is "dirty" if there are one or
* more validation errors present.
*
* This record is also available when using data binding as a "pseudo-association" called
* "validation". This pseudo-association can be hidden by an explicitly declared
* association by the same name (for compatibility reasons), but doing so is not
* recommended.
*
* The `{@link Ext.Component#modelValidation}` config can be used to enable automatic
* binding from the "validation" of a record to the form fields that may be bound to its
* values.
*
* # Associations
*
* Models often have associations with other Models. These associations can be defined by
* fields (often called "foreign keys") or by other data such as a many-to-many (or "matrix").
* See {@link Ext.data.schema.Association} for information about configuring and using associations.
*
* # 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',
* 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 accomplished with the static `load` method:
*
* //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!');
* }
* });
*
* # HTTP Parameter names when using a {@link Ext.data.proxy.Ajax Ajax proxy}
*
* By default, the model ID is specified in an HTTP parameter named `id`. To change the
* name of this parameter use the Proxy's {@link Ext.data.proxy.Ajax#idParam idParam}
* configuration.
*
* Parameters for other commonly passed values such as
* {@link Ext.data.proxy.Ajax#pageParam page number} or
* {@link Ext.data.proxy.Ajax#startParam start row} may also be configured.
*
* # 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',
requires: [
'Ext.data.ErrorCollection',
'Ext.data.operation.*',
'Ext.data.field.*',
'Ext.data.validator.Validator',
'Ext.data.schema.Schema',
'Ext.data.identifier.Generator',
'Ext.data.identifier.Sequential'
],
uses: [
'Ext.data.Validation'
],
/**
* @property {Boolean} isEntity
* The value `true` to identify this class and its subclasses.
* @readonly
*/
isEntity: true,
/**
* @property {Boolean} isModel
* The value `true` to identify this class and its subclasses.
* @readonly
*/
isModel: true,
// Record ids are more flexible.
validIdRe: null,
erasing: false,
observableType: 'record',
/**
* @property {"C"/"R"/"U"/"D"} crudState
* This value is initially "R" or "C" indicating the initial CRUD state. As the
* record changes and the various joined parties (stores, sessions, etc.) are notified
* this value is updated prior to these calls. In other words, the joined parties
* are notified after the `crudState` is updated. This means that the `crudState`
* property may be briefly out of sync with underlying changes if this state is used
* outside of these notifications.
*
* The possible states have these meanings:
*
* * "R" - The record is in a cleanly retrieved (unmodified) state.
* * "C" - The record is in a newly created (`phantom`) state.
* * "U" - The record is in an updated, `modified` (`dirty`) state.
* * "D" - The record is in a `dropped` state.
*
* @readonly
* @protected
* @since 6.2.0
*/
crudState: 'R',
/**
* @property {"C"/"R"/"U"/"D"} crudStateWas
* This value is initially `null` indicating there is no previous CRUD state. As the
* record changes and the various joined parties (stores, sessions, etc.) are notified
* this value is updated for the *subsequent* calls. In other words, the joined parties
* are notified and then `crudStateWas` is modified for the next update.
*
* The value of this property has the same meaning as `crudState`.
*
* @readonly
* @protected
* @since 6.2.0
*/
crudStateWas: null,
constructor: function (data, session) {
var me = this,
cls = me.self,
identifier = cls.identifier,
Model = Ext.data.Model,
modelIdentifier = Model.identifier,
idProperty = me.idField.name,
array, id, initializeFn, internalId, len, i, fields;
// Yes, this is here on purpose. See EXTJS-16494. The second
// assignment seems to work around a strange JIT issue that prevents
// this.data being assigned in random scenarios, even though the data
// is passed into the constructor. The issue occurs on 4th gen iPads and
// lower, possibly other older iOS devices.
// A similar issue can occur with the hasListeners property of Observable
// (see the constructor of Ext.mixin.Observable)
me.data = me.data = data || (data = {});
me.session = session || null;
me.internalId = internalId = modelIdentifier.generate();
//<debug>
var dataId = data[idProperty];
if (session && !session.isSession) {
Ext.raise('Bad Model constructor argument 2 - "session" is not a Session');
}
//</debug>
if ((array = data) instanceof Array) {
me.data = data = {};
fields = me.getFields();
len = Math.min(fields.length, array.length);
for (i = 0; i < len; ++i) {
data[fields[i].name] = array[i];
}
}
if (!(initializeFn = cls.initializeFn)) {
cls.initializeFn = initializeFn = Model.makeInitializeFn(cls);
}
if (!initializeFn.$nullFn) {
cls.initializeFn(me);
}
// Must do this after running the initializeFn due to converters on idField
if (!(me.id = id = data[idProperty]) && id !== 0) {
//<debug>
if (dataId) {
Ext.raise('The model ID configured in data ("' + dataId + '") has been rejected by the ' + me.fieldsMap[idProperty].type + ' field converter for the ' + idProperty + ' field');
}
//</debug>
if (session) {
identifier = session.getIdentifier(cls);
id = identifier.generate();
} else if (modelIdentifier === identifier) {
id = internalId;
} else {
id = identifier.generate();
}
data[idProperty] = me.id = id;
me.phantom = true;
me.crudState = 'C';
}
if (session) {
session.add(me);
}
if (me.init && Ext.isFunction(me.init)) {
me.init();
}
},
/**
* @property {String} entityName
* The short name of this entity class. This name is derived from the `namespace` of
* the associated `schema` and this class name. By default, a class is not given a
* shortened name.
*
* All entities in a given `schema` must have a unique `entityName`.
*
* For more details see "Relative Naming" in {@link Ext.data.schema.Schema}.
*/
/**
* @property {Boolean} editing
* Internal flag used to track whether or not the model instance is currently being edited.
* @readonly
*/
editing: false,
/**
* @property {Boolean} dirty
* True if this record has been modified.
* @readonly
*/
dirty: false,
/**
* @property {Ext.data.Session} session
* The {@link Ext.data.Session} for this record.
* @readonly
*/
session: null,
/**
* @property {Boolean} dropped
* True if this record is pending delete on the server. This is set by the `drop`
* method and transmitted to the server by the `save` method.
* @readonly
*/
dropped: false,
/**
* @property {Boolean} erased
* True if this record has been erased on the server. This flag is set of the `erase`
* method.
* @readonly
*/
erased: false,
/**
* @cfg {String} [clientIdProperty]
* The name of the property a server will use to send back a client-generated id in a
* `create` or `update` `{@link Ext.data.operation.Operation operation}`.
*
* If specified, this property cannot have the same name as any other field.
*
* For example:
*
* Ext.define('Person', {
* idProperty: 'id', // this is the default value (for clarity)
*
* clientIdProperty: 'clientId',
*
* identifier: 'negative', // to generate -1, -2 etc on the client
*
* fields: [ 'name' ]
* });
*
* var person = new Person({
* // no id provided, so -1 is generated
* name: 'Clark Kent'
* });
*
* The server is given this data during the `create`:
*
* {
* id: -1,
* name: 'Clark Kent'
* }
*
* The server allocates a real id and responds like so:
*
* {
* id: 427,
* clientId: -1
* }
*
* This property is most useful when creating multiple entities in a single call to
* the server in a `{@link Ext.data.operation.Create create operation}`. Alternatively,
* the server could respond with records that correspond one-to-one to those sent in
* the `operation`.
*
* For example the client could send a `create` with this data:
*
* [ { id: -1, name: 'Clark Kent' },
* { id: -2, name: 'Peter Parker' },
* { id: -3, name: 'Bruce Banner' } ]
*
* And the server could respond in the same order:
*
* [ { id: 427 }, // updates id = -1
* { id: 428 }, // updates id = -2
* { id: 429 } ] // updates id = -3
*
* Or using `clientIdProperty` the server could respond in arbitrary order:
*
* [ { id: 427, clientId: -3 },
* { id: 428, clientId: -1 },
* { id: 429, clientId: -2 } ]
*
* **IMPORTANT:** When upgrading from previous versions be aware that this property
* used to perform the role of `{@link Ext.data.writer.Writer#clientIdProperty}` as
* well as that described above. To continue send a client-generated id as other than
* the `idProperty`, set `clientIdProperty` on the `writer`. A better solution, however,
* is most likely a properly configured `identifier` as that would work better with
* associations.
*/
clientIdProperty: null,
evented: false,
/**
* @property {Boolean} phantom
* True when the record does not yet exist in a server-side database. Any record which
* has a real database identity set as its `idProperty` is NOT a phantom -- it's real.
*/
phantom: false,
/**
* @cfg {String} [idProperty='id']
* The name of the field treated as this Model's unique id.
*
* If changing the idProperty in a subclass, the generated id field will replace the one
* generated by the superclass, for example;
*
* Ext.define('Super', {
* extend: 'Ext.data.Model',
* fields: ['name']
* });
*
* Ext.define('Sub', {
* extend: 'Super',
* idProperty: 'customId'
* });
*
* var fields = Super.getFields();
* // Has 2 fields, "name" & "id"
* console.log(fields[0].name, fields[1].name, fields.length);
*
* fields = Sub.getFields();
* // Has 2 fields, "name" & "customId", "id" is replaced
* console.log(fields[0].name, fields[1].name, fields.length);
*
* The data values for this field must be unique or there will be id value collisions
* in the {@link Ext.data.Store Store}.
*/
idProperty: 'id',
/**
* @cfg {Object} manyToMany
* A config object for a {@link Ext.data.schema.ManyToMany ManyToMany} association.
* See the class description for {@link Ext.data.schema.ManyToMany ManyToMany} for
* configuration examples.
*/
manyToMany: null,
/**
* @cfg {String/Object} identifier
* The id generator to use for this model. The `identifier` generates values for the
* {@link #idProperty} when no value is given. Records with client-side generated
* values for {@link #idProperty} are called {@link #phantom} records since they are
* not yet known to the server.
*
* This can be overridden at the model level to provide a custom generator for a model.
* The simplest form of this would be:
*
* Ext.define('MyApp.data.MyModel', {
* extend: 'Ext.data.Model',
* requires: ['Ext.data.identifier.Sequential'],
* identifier: 'sequential',
* ...
* });
*
* The above would generate {@link Ext.data.identifier.Sequential sequential} id's such
* as 1, 2, 3 etc..
*
* Another useful id generator is {@link Ext.data.identifier.Uuid}:
*
* Ext.define('MyApp.data.MyModel', {
* extend: 'Ext.data.Model',
* requires: ['Ext.data.identifier.Uuid'],
* identifier: 'uuid',
* ...
* });
*
* An id generator can also be further configured:
*
* Ext.define('MyApp.data.MyModel', {
* extend: 'Ext.data.Model',
* identifier: {
* type: 'sequential',
* seed: 1000,
* prefix: 'ID_'
* }
* });
*
* The above would generate id's such as ID_1000, ID_1001, ID_1002 etc..
*
* If multiple models share an id space, a single generator can be shared:
*
* Ext.define('MyApp.data.MyModelX', {
* extend: 'Ext.data.Model',
* identifier: {
* type: 'sequential',
* id: 'xy'
* }
* });
*
* Ext.define('MyApp.data.MyModelY', {
* extend: 'Ext.data.Model',
* identifier: {
* type: 'sequential',
* id: 'xy'
* }
* });
*
* For more complex, shared id generators, a custom generator is the best approach.
* See {@link Ext.data.identifier.Generator} for details on creating custom id generators.
*/
identifier: null,
// Fields config and property
// @cmd-auto-dependency {aliasPrefix: "data.field."}
/**
* @cfg {Object[]/String[]} fields
* An Array of `Ext.data.field.Field` config objects, simply the field
* {@link Ext.data.field.Field#name name}, or a mix of config objects and strings.
* If just a name is given, the field type defaults to `auto`.
*
* In a {@link Ext.data.field.Field Field} config object you may pass the alias of
* the `Ext.data.field.*` type using the `type` config option.
*
* // two fields are set:
* // - an 'auto' field with a name of 'firstName'
* // - and an Ext.data.field.Integer field with a name of 'age'
* fields: ['firstName', {
* type: 'int',
* name: 'age'
* }]
*
* Fields will automatically be created at read time for any for any keys in the
* data passed to the Model's {@link #proxy proxy's}
* {@link Ext.data.reader.Reader reader} whose name is not explicitly configured in
* the `fields` config.
*
* Extending a Model class will inherit all the `fields` from the superclass /
* ancestor classes.
*/
/**
* @property {Ext.data.field.Field[]} fields
* An array fields defined for this Model (including fields defined in superclasses)
* in ordinal order; that is in declaration order.
* @private
* @readonly
*/
/**
* @property {Object} fieldOrdinals
* This property is indexed by field name and contains the ordinal of that field. The
* ordinal often has meaning to servers and is derived based on the position in the
* `fields` array.
*
* This can be used like so:
*
* Ext.define('MyApp.models.User', {
* extend: 'Ext.data.Model',
*
* fields: [
* { name: 'name' }
* ]
* });
*
* var nameOrdinal = MyApp.models.User.fieldOrdinals.name;
*
* // or, if you have an instance:
*
* var user = new MyApp.models.User();
* var nameOrdinal = user.fieldOrdinals.name;
*
* @private
* @readonly
*/
/**
* @property {Object} modified
* A hash of field values which holds the initial values of fields before a set of edits
* are {@link #commit committed}.
*/
/**
* @property {Object} previousValues
* This object is similar to the `modified` object except it holds the data values as
* they were prior to the most recent change.
* @readonly
* @private
*/
previousValues: undefined, // Not "null" so getPrevious returns undefined first time
// @cmd-auto-dependency { aliasPrefix : "proxy.", defaultPropertyName : "defaultProxyType"}
/**
* @cfg {String/Object/Ext.data.proxy.Proxy} proxy
* The {@link Ext.data.proxy.Proxy proxy} to use for this class.
*/
proxy: undefined,
/**
* @cfg {String/Object} [schema='default']
* The name of the {@link Ext.data.schema.Schema schema} to which this entity and its
* associations belong. For details on custom schemas see `Ext.data.schema.Schema`.
*/
/**
* @property {Ext.data.schema.Schema} schema
* The `Ext.data.schema.Schema` to which this entity and its associations belong.
* @readonly
*/
schema: 'default',
/**
* @cfg {String} [versionProperty]
* If specified, this is the name of the property that contains the entity "version".
* The version property is used to manage a long-running transaction and allows the
* detection of simultaneous modification.
*
* The way a version property is used is that the client receives the version as it
* would any other entity property. When saving an entity, this property is always
* included in the request and the server uses the value in a "conditional update".
* If the current version of the entity on the server matches the version property
* sent by the client, the update is allowed. Otherwise, the update fails.
*
* On successful update, both the client and server increment the version. This is
* done on the server in the conditional update and on the client when it receives a
* success on its update request.
*/
versionProperty: null,
/**
* @property {Number} generation
* This property is incremented on each modification of a record.
* @readonly
* @since 5.0.0
*/
generation: 1,
/**
* @cfg {Object[]} validators
* An array of {@link Ext.data.validator.Validator validators} for this model.
*/
/**
* @cfg {String} [validationSeparator=null]
* If specified this property is used to concatenate multiple errors for each field
* as reported by the `validators`.
*/
validationSeparator: null,
/**
* @cfg {Boolean} [convertOnSet=true]
* Set to `false` to prevent any converters from being called on fields specified in
* a {@link Ext.data.Model#set set} operation.
*
* **Note:** Setting the config to `false` will only prevent the convert / calculate
* call when the set `fieldName` param matches the field's `{@link #name}`. In the
* following example the calls to set `salary` will not execute the convert method
* on `set` while the calls to set `vested` will execute the convert method on the
* initial read as well as on `set`.
*
* Example model definition:
*
* Ext.define('MyApp.model.Employee', {
* extend: 'Ext.data.Model',
* fields: ['yearsOfService', {
* name: 'salary',
* convert: function (val) {
* var startingBonus = val * .1;
* return val + startingBonus;
* }
* }, {
* name: 'vested',
* convert: function (val, record) {
* return record.get('yearsOfService') >= 4;
* },
* depends: 'yearsOfService'
* }],
* convertOnSet: false
* });
*
* var tina = Ext.create('MyApp.model.Employee', {
* salary: 50000,
* yearsOfService: 3
* });
*
* console.log(tina.get('salary')); // logs 55000
* console.log(tina.get('vested')); // logs false
*
* tina.set({
* salary: 60000,
* yearsOfService: 4
* });
* console.log(tina.get('salary')); // logs 60000
* console.log(tina.get('vested')); // logs true
*/
convertOnSet: true,
// Associations configs and properties
/**
* @cfg {Object[]} associations
* An array of {@link Ext.data.schema.Association associations} for this model.
*
* For further documentation, see {@link Ext.data.schema.Association}.
*
* @deprecated 6.2.0 Use `hasMany/hasOne/belongsTo`.
*/
/**
* @cfg {String/Object/String[]/Object[]} hasMany
* One or more `Ext.data.schema.HasMany` associations for this model.
*/
/**
* @cfg {String/Object/String[]/Object[]} hasOne
* One or more `Ext.data.schema.HasOne` associations for this model.
*/
/**
* @cfg {String/Object/String[]/Object[]} belongsTo
* One or more `Ext.data.schema.BelongsTo` associations for this model.
*/
/**
* 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 `endEdit` or `cancelEdit`.
*/
beginEdit: function () {
var me = this,
modified = me.modified,
previousValues = me.previousValues;
if (!me.editing) {
me.editing = true;
me.editMemento = {
dirty: me.dirty,
data: Ext.apply({}, me.data),
generation: me.generation,
modified: modified && Ext.apply({}, modified),
previousValues: previousValues && Ext.apply({}, previousValues)
};
}
},
/**
* Cancels all changes made in the current edit operation.
*/
cancelEdit: function () {
var me = this,
editMemento = me.editMemento;
if (editMemento) {
me.editing = false;
// reset the modified state, nothing changed since the edit began
Ext.apply(me, editMemento);
me.editMemento = null;
}
},
/**
* 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 any stores of the change.
* @param {String[]} [modifiedFieldNames] Array of field names changed during edit.
*/
endEdit: function (silent, modifiedFieldNames) {
var me = this,
editMemento = me.editMemento;
if (editMemento) {
me.editing = false;
me.editMemento = null;
// Since these reflect changes we never notified others about, the real set
// of "previousValues" is what we captured in the memento:
me.previousValues = editMemento.previousValues;
if (!silent) {
if (!modifiedFieldNames) {
modifiedFieldNames = me.getModifiedFieldNames(editMemento.data);
}
if (me.dirty || (modifiedFieldNames && modifiedFieldNames.length)) {
me.callJoined('afterEdit', [modifiedFieldNames]);
}
}
}
},
getField: function (name) {
return this.self.getField(name);
},
/**
* Get the fields array for this model.
* @return {Ext.data.field.Field[]} The fields array
*/
getFields: function () {
return this.self.getFields();
},
getFieldsMap: function () {
return this.fieldsMap;
},
/**
* Get the idProperty for this model.
* @return {String} The idProperty
*/
getIdProperty: function () {
return this.idProperty;
},
/**
* Returns the unique ID allocated to this model instance as defined by `idProperty`.
* @return {Number/String} The id
*/
getId: function () {
return this.id;
},
/**
* Return a unique observable ID. Model is not observable but tree nodes (`Ext.data.NodeInterface`) are, so
* they must be globally unique within the {@link #observableType}.
* @protected
*/
getObservableId: function() {
return this.internalId;
},
/**
* Sets the model instance's id field to the given id.
* @param {Number/String} id The new id.
* @param {Object} [options] See {@link #set}.
*/
setId: function (id, options) {
this.set(this.idProperty, id, options);
},
/**
* This method returns the value of a field given its name prior to its most recent
* change.
* @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
* @return {Object} The value of the given field prior to its current value. `undefined`
* if there is no previous value;
*/
getPrevious: function (fieldName) {
var previousValues = this.previousValues;
return previousValues && previousValues[fieldName];
},
/**
* Returns true if the passed field name has been `{@link #modified}` since the load or last commit.
* @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
* @return {Boolean}
*/
isModified: function (fieldName) {
var modified = this.modified;
return !!(modified && modified.hasOwnProperty(fieldName));
},
/**
* Returns the original value of a modified field. If there is no modified value,
* `undefined` will be return. Also see {@link #isModified}.
* @param {String} fieldName The name of the field for which to return the original value.
* @return {Object} modified
*/
getModified: function (fieldName) {
var out;
if (this.isModified(fieldName)) {
out = this.modified[fieldName];
}
return out;
},
/**
* Returns the value of the given field.
* @param {String} fieldName The name of the field.
* @return {Object} The value of the specified field.
*/
get: function (fieldName) {
return this.data[fieldName];
},
// This object is used whenever the set() method is called and given a string as the
// first argument. This approach saves memory (and GC costs) since we could be called
// a lot.
_singleProp: {},
_rejectOptions: {
convert: false,
silent: true
},
/**
* Sets the given field to the given value. For example:
*
* record.set('name', 'value');
*
* This method can also be passed an object containing multiple values to set at once.
* For example:
*
* record.set({
* name: 'value',
* age: 42
* });
*
* The following store events are fired when the modified record belongs to a store:
*
* - {@link Ext.data.Store#event-beginupdate beginupdate}
* - {@link Ext.data.Store#event-update update}
* - {@link Ext.data.Store#event-endupdate endupdate}
*
* @param {String/Object} fieldName The field to set, or an object containing key/value
* pairs.
* @param {Object} newValue The value for the field (if `fieldName` is a string).
* @param {Object} [options] Options for governing this update.
* @param {Boolean} [options.convert=true] Set to `false` to prevent any converters from
* being called during the set operation. This may be useful when setting a large bunch of
* raw values.
* @param {Boolean} [options.dirty=true] Pass `false` if the field values are to be
* understood as non-dirty (fresh from the server). When `true`, this change will be
* reflected in the `modified` collection.
* @param {Boolean} [options.commit=false] Pass `true` to call the {@link #commit} method
* after setting fields. If this option is passed, the usual after change processing will
* be bypassed. {@link #commit Commit} will be called even if there are no field changes.
* @param {Boolean} [options.silent=false] Pass `true` to suppress notification of any
* changes made by this call. Use with caution.
* @return {String[]} The array of modified field names or null if nothing was modified.
*/
set: function (fieldName, newValue, options) {
var me = this,
cls = me.self,
data = me.data,
modified = me.modified,
prevVals = me.previousValues,
session = me.session,
single = Ext.isString(fieldName),
opt = (single ? options : newValue),
convertOnSet = opt ? opt.convert !== false : me.convertOnSet,
fieldsMap = me.fieldsMap,
silent = opt && opt.silent,
commit = opt && opt.commit,
updateRefs = !(opt && opt.refs === false) && session,
// Don't need to do dirty processing with commit, since we'll always
// end up with nothing modified and not dirty
dirty = !(opt && opt.dirty === false && !commit),
modifiedFieldNames = null,
dirtyRank = 0,
associations = me.associations,
currentValue, field, idChanged, key, name, oldId, comparator, dep, dependents,
i, numFields, newId, rankedFields, reference, value, values, roleName;
if (single) {
values = me._singleProp;
values[fieldName] = newValue;
} else {
values = fieldName;
}
if (!(rankedFields = cls.rankedFields)) {
// On the first edit of a record of this type we need to ensure we have the
// topo-sort done:
rankedFields = cls.rankFields();
}
numFields = rankedFields.length;
do {
for (name in values) {
value = values[name];
currentValue = data[name];
comparator = me;
field = fieldsMap[name];
if (field) {
if (convertOnSet && field.convert) {
value = field.convert(value, me);
}
comparator = field;
reference = field.reference;
} else {
reference = null;
}
if (comparator.isEqual(currentValue, value)) {
continue; // new value is the same, so no change...
}
data[name] = value;
(modifiedFieldNames || (modifiedFieldNames = [])).push(name);
(prevVals || (me.previousValues = prevVals = {}))[name] = currentValue;
// We need the cls to be present because it means the association class is loaded,
// otherwise it could be pending.
if (reference && reference.cls) {
if (updateRefs) {
session.updateReference(me, field, value, currentValue);
}
reference.onValueChange(me, session, value, currentValue);
}
i = (dependents = field && field.dependents) && dependents.length;
while (i-- > 0) {
// we use the field instance to hold the dirty bit to avoid any
// extra allocations... we'll clear this before we depart. We do
// this so we can perform the fewest recalculations possible as
// each dependent field only needs to be recalculated once.
(dep = dependents[i]).dirty = true;
dirtyRank = dirtyRank ? Math.min(dirtyRank, dep.rank) : dep.rank;
}
if (!field || field.persist) {
if (modified && modified.hasOwnProperty(name)) {
if (!dirty || comparator.isEqual(modified[name], value)) {
// The original value in me.modified equals the new value, so
// the field is no longer modified:
delete modified[name];
me.dirty = -1; // fix me.dirty later (still truthy)
}
} else if (dirty) {
if (!modified) {
me.modified = modified = {}; // create only when needed
}
me.dirty = true;
modified[name] = currentValue;
}
}
if (name === me.idField.name) {
idChanged = true;
oldId = currentValue;
newId = value;
}
}
if (!dirtyRank) {
// Unless there are dependent fields to process we can break now. This is
// what will happen for all code pre-dating the depends or simply not
// using it, so it will add very little overhead when not used.
break;
}
// dirtyRank has the minimum rank (a 1-based value) of any dependent field
// that needs recalculating due to changes above. The way we go about this
// is to use our helper object for processing single argument invocations
// to process just this one field. This is because the act of setting it
// may cause another field to be invalidated, so while we cannot know at
// this moment all the fields we need to recalculate, we know that only
// those following this field in rankedFields can possibly be among them.
field = rankedFields[dirtyRank - 1]; // dirtyRank is 1-based
field.dirty = false; // clear just this field's dirty state
if (single) {
delete values[fieldName]; // cleanup last value
} else {
values = me._singleProp; // switch over
single = true;
}
fieldName = field.name;
values[fieldName] = data[fieldName];
// We are now processing a dependent field, so we want to force a
// convert to occur because it's the only way it will get a value
convertOnSet = true;
// Since dirtyRank is 1-based and refers to the field we need to handle
// on this pass, we can treat it like an index for a minute and look at
// the next field on towards the end to find the index of the next dirty
// field.
for ( ; dirtyRank < numFields; ++dirtyRank) {
if (rankedFields[dirtyRank].dirty) {
break;
}
}
if (dirtyRank < numFields) {
// We found a field after this one marked as dirty so make the index
// a proper 1-based rank:
++dirtyRank;
} else {
// We did not find any more dirty fields after this one, so clear the
// dirtyRank and we will perhaps fall out after the next update
dirtyRank = 0;
}
} while (1);
if (me.dirty < 0) {
// 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;
}
}
}
if (single) {
// cleanup our reused object for next time... important to do this before
// we fire any events or call anyone else (like afterEdit)!
delete values[fieldName];
}
++me.generation;
if (idChanged) {
me.id = newId;
me.onIdChanged(newId, oldId);
me.callJoined('onIdChanged', [oldId, newId]);
if (associations) {
for (roleName in associations) {
associations[roleName].onIdChanged(me, oldId, newId);
}
}
}
if (commit) {
me.commit(silent, modifiedFieldNames);
} else if (!silent && !me.editing && modifiedFieldNames) {
me.callJoined('afterEdit', [modifiedFieldNames]);
}
return modifiedFieldNames;
},
/**
* 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#event-update} event to have their code notified of reject
* operations.
*
* @param {Boolean} [silent=false] `true` to skip notification of the owning store of the change.
*/
reject: function (silent) {
var me = this,
modified = me.modified;
//<debug>
if (me.erased) {
Ext.raise('Cannot reject once a record has been erased.');
}
//</debug>
if (modified) {
me.set(modified, me._rejectOptions);
}
me.dropped = false;
me.clearState();
if (!silent) {
me.callJoined('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#event-update} event to have their code notified of commit
* operations.
*
* @param {Boolean} [silent=false] Pass `true` to skip notification of the owning store of the change.
* @param {String[]} [modifiedFieldNames] Array of field names changed during sync with server if known.
* Omit or pass `null` if unknown. An empty array means that it is known that no fields were modified
* by the server's response.
* Defaults to false.
*/
commit: function (silent, modifiedFieldNames) {
var me = this,
versionProperty = me.versionProperty,
data = me.data,
erased;
me.clearState();
if (versionProperty && !me.phantom && !isNaN(data[versionProperty])) {
++data[versionProperty];
}
me.phantom = false;
if (me.dropped) {
me.erased = erased = true;
}
if (!silent) {
if (erased) {
me.callJoined('afterErase');
} else {
me.callJoined('afterCommit', [modifiedFieldNames]);
}
}
},
clearState: function() {
var me = this;
me.dirty = me.editing = false;
me.editMemento = me.modified = null;
},
/**
* Marks this record as `dropped` and waiting to be deleted on the server. When a
* record is dropped, it is automatically removed from all association stores and
* any child records associated to this record are also dropped (a "cascade delete")
* depending on the `cascade` parameter.
*
* @param {Boolean} [cascade=true] Pass `false` to disable the cascade to drop child
* records.
* @since 5.0.0
*/
drop: function (cascade) {
var me = this,
associations = me.associations,
session = me.session,
roleName;
if (me.erased || me.dropped) {
return;
}
me.dropped = true;
if (associations && cascade !== false) {
for (roleName in associations) {
associations[roleName].onDrop(me, session);
}
}
me.callJoined('afterDrop');
if (me.phantom) {
me.setErased();
}
},
/**
* Tells this model instance that an observer is looking at it.
* @param {Ext.data.Store} item The store to which this model has been added.
*/
join: function (item) {
var me = this,
joined = me.joined;
// Optimize this, gets called a lot
if (!joined) {
joined = me.joined = [item];
} else if (!joined.length) {
joined[0] = item;
} else {
// TODO: do we need joined here? Perhaps push will do.
Ext.Array.include(joined, item);
}
if (item.isStore && !me.store) {
/**
* @property {Ext.data.Store} store
* The {@link Ext.data.Store Store} to which this instance belongs.
*
* **Note:** If this instance is bound to multiple stores, this property
* will reference only the first.
*/
me.store = item;
}
},
/**
* Tells this model instance that it has been removed from the store.
* @param {Ext.data.Store} store The store from which this model has been removed.
*/
unjoin: function (item) {
var me = this,
joined = me.joined,
// TreeModels are never joined to their TreeStore.
// But unjoin is called by the base class's onCollectionRemove, so joined may be undefined.
len = joined && joined.length,
store = me.store,
i;
if (item === me.session) {
me.session = null;
} else {
if (len === 1 && joined[0] === item) {
joined.length = 0;
} else if (len) {
Ext.Array.remove(joined, item);
}
if (store === item) {
store = null;
if (joined) {
for (i = 0, len = joined.length; i < len; ++i) {
item = joined[i];
if (item.isStore) {
store = item;
break;
}
}
}
me.store = store;
}
}
},
/**
* Creates a clone of this record. States like `dropped`, `phantom` and `dirty` are
* all preserved i