UNPKG

extjs-gpl

Version:

GPL licensed version of Sencha Ext JS

1,372 lines (1,258 loc) 130 kB
/** * 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