/** * 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: '[email protected]'}); * * 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, loadOperation: null, loadCount: 0, 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, skipStoreAddition) { 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.internalId = internalId = modelIdentifier.generate(); //<debug> var dataId = data[idProperty]; // eslint-disable-line vars-on-top, one-var 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.isSummaryModel) { 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 && !skipStoreAddition) { session.add(me); } // Needs to be set after the add to the session if (me.phantom) { me.crudStateWas = 'C'; } } 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 * 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. * * By default, the proxy is configured from the {@link Ext.data.schema.Schema schema}. * You can ignore the schema defaults by setting `schema: false` on the `proxy` config. * * Ext.define('MyApp.data.CustomProxy', { * extend: 'Ext.data.proxy.Ajax', * alias: 'proxy.customproxy', * * url: 'users.json' * }); * * Ext.define('MyApp.models.CustomModel', { * extend: 'Ext.data.Model', * * fields: ['name'], * proxy: { * type: 'customproxy, * schema: false * } * }); * * With `schema: false`, the `url` of the proxy will be used instead of what has been defined * on the schema. */ 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 {Object} summary * Summary fields are a special kind of field that is used to assist in creating an * aggregation for this model. A new model type that extends this model will be * created, accessible via {@link #method-getSummaryModel}. This summary model will * have these virtual aggregate fields in the fields collection like a normal model. * Each key in the object is the field name. The value for each field should mirror * the {@link #cfg-fields}, excluding the `name` option. The summary model generated * will have 2 fields, 'rate', which will aggregate using an average and maxRate, * which will aggregate using the maximum value. * * See {@link Ext.data.summary.Base} for more information. * * Ext.define('User', { * extend: 'Ext.data.Model', * fields: [{ * name: 'rate', * summary: 'avg' * }], * * summary: { * maxRate: { * field: 'rate', // calculated from rate field * summary: 'max' * } * } * }); * * @since 6.5.0 */ summary: null, /** * @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 * If specified this property is used to concatenate multiple errors for each field * as reported by the `validators`. */ validationSeparator: null, /** * @cfg {Boolean} convertOnSet * 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) }; } }, /** * Calculate all summary fields on this record. * @param {Ext.data.Model[]} records The records to use for calculation. * * @since 6.5.0 */ calculateSummary: function(records) { var fields = this.getFields(), len = fields.length, recLen = records.length, i, result, summary, prop, name, field; for (i = 0; i < len; ++i) { field = fields[i]; summary = field.getSummary(); if (summary) { result = result || {}; name = field.name; prop = field.summaryField || name; result[name] = summary.calculate(records, prop, 'data', 0, recLen); } } if (result) { this.set(result, this._commitOptions); } }, /** * Cancels all changes made in the current edit operation. */ cancelEdit: function() { var me = this, editMemento = me.editMemento, validation = me.validation; if (editMemento) { me.editing = false; // reset the modified state, nothing changed since the edit began Ext.apply(me, editMemento); me.editMemento = null; if (validation && validation.syncGeneration !== me.generation) { validation.syncGeneration = 0; } } }, /** * 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); // eslint-disable-line no-constant-condition 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} owner The store or other owner object to which this model * has been added. */ join: function(owner) { var me = this, joined = me.joined; // Optimize this, gets called a lot if (!joined) { joined = me.joined = [owner]; } else if (!joined.length) { joined[0] = owner; } else { // TODO: do we need joined here? Perhaps push will do. Ext.Array.include(joined, owner); } if (owner.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 = owner; } }, /** * Tells this model instance that it has been removed from the store. * @param {Ext.data.Store} owner The store or other owner object from which this * model has been removed. */ unjoin: function(owner) { 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 (owner === me.session) { me.session = null; } else { if (len === 1 && joined[0] === owner) { joined.length = 0; } else if (len) { Ext.Array.remove(joined, owner); } if (store === owner) { store = null; if (joined) { for (i = 0, len = joined.length; i < len; ++i) { owner = joined[i]; if (owner.isStore) { store = owner; break; } } } me.store = store; } } }, /** * Creates a clone of this record. States like `dropped`, `phantom` and `dirty` are * all preserved in the cloned record. * * @param {Ext.data.Session} [session] The session to which the new record * belongs. * @return {Ext.data.Model} The cloned record. */ clone: function(session) { var me = this, modified = me.modified, ret = me.copy(me.id, session); if (modified) { // Restore the modified fields state ret.modified = Ext.apply({}, modified); } ret.dirty = me.dirty; ret.dropped = me.dropped; ret.phantom = me.phantom; return ret; }, /** * Creates a clean copy of this record. The returned record will not consider any its * fields as modified. * * To generate a phantom instance with a new id pass `null`: * * var rec = record.copy(null); // clone the record but no id (one is generated) * * @param {String} [newId] A new id, defaults to the id of the instance being copied. * See `{@link Ext.data.Model#idProperty idProperty}`. * @param {Ext.data.Session} [session] The session to which the new record * belongs. * * @return {Ext.data.Model} */ copy: function(newId, session) { var me = this, data = Ext.apply({}, me.data), idProperty = me.idProperty, T = me.self; if (newId || newId === 0) { data[idProperty] = newId; } else if (newId === null) { delete data[idProperty]; } return new T(data, session); }, /** * Returns the configured Proxy for this Model. * @return {Ext.data.proxy.Proxy} The proxy */ getProxy: function() { return this.self.getProxy(); }, /** * Returns the `Ext.data.Validation` record holding the results of this record's * `validators`. This record is lazily created on first request and is then kept on * this record to be updated later. * * See the class description for more about `validators`. * * @param {Boolean} [refresh] Pass `false` to not call the `refresh` method on the * validation instance prior to returning it. Pass `true` to force a `refresh` of the * validation instance. By default the returned record is only refreshed if changes * have been made to this record. * @return {Ext.data.Validation} The `Validation` record for this record. * @since 5.0.0 */ getValidation: function(refresh) { var me = this, ret = me.validation; if (!ret) { me.validation = ret = new Ext.data.Validation(); ret.attach(me); } if (refresh === true || (refresh !== false && ret.syncGeneration !== me.generation)) { ret.refresh(refresh); } return ret; }, /** * Validates the current data against all of its configured {@link #validators}. The * returned collection holds an object for each reported problem from a `validator`. * * @return {Ext.data.ErrorCollection} The errors collection. * @deprecated 5.0 Use `getValidation` instead. */ validate: function() { return new Ext.data.ErrorCollection().init(this); }, /** * Checks if the model is valid. See {@link #getValidation}. * @return {Boolean} True if the model is valid. */ isValid: function() { return this.getValidation().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(); }, /** * @method erase * @localdoc Destroys the model using the configured proxy. The erase action is * asynchronous. Any processing of the erased record should be done in a callback. * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: [ * {name: 'id', type: 'int'}, * {name: 'name', type: 'string'} * ], * proxy: { * type: 'ajax', * url: 'server.url' * } * }); * * var user = new MyApp.model.User({ * name: 'Foo' * }); * * // pass the phantom record data to the server to be saved * user.save({ * success: function(record, operation) { * // do something if the save succeeded * // erase the created record * record.erase({ * failure: function(record, operation) { * // do something if the erase failed * }, * success: function(record, operation) { * // do something if the erase succeeded * }, * callback: function(record, operation, success) { * // do something if the erase succeeded or failed * } * }); * } * }); * * **NOTE:** If a {@link #phantom} record is erased it will not be processed via the * proxy. However, any passed `success` or `callback` functions will be called. * * The options param is an {@link Ext.data.operation.Destroy} config object * containing success, failure and callback functions, plus optional scope. * * @inheritdoc #method-load * @return {Ext.data.operation.Destroy} The destroy operation */ erase: function(options) { var me = this; me.erasing = true; // Drop causes a removal from the backing Collection. // The store's onCollectionRemove will respond to this by adding the record to // its "to remove" stack and setting its needsSync // flag unless the above "erasing" flag is set. me.drop(); me.erasing = false; return me.save(options); }, setErased: function() { this.erased = true; this.callJoined('afterErase'); }, /** * Gets an object of only the fields that have been modified since this record was * created or committed. Only persistent fields are tracked in the `modified` set so * this method will only return changes to persistent fields. * * For more control over the returned data, see `{@link #getData}`. * @return {Object} */ getChanges: function() { return this.getData(this._getChangesOptions); }, /** * Returns the array of fields that are declared as critical (must always send). * @return {Ext.data.field.Field[]} */ getCriticalFields: function() { var cls = this.self, ret = cls.criticalFields; if (!ret) { cls.rankFields(); ret = cls.criticalFields; } return ret; }, /** * This method is called by the {@link Ext.data.reader.Reader} after loading a model from * the server. This is after processing any inline associations that are available. * * @method onLoad * * @protected * @template */ /** * 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: [ * ... * ] * } * ] * } * * @param {Object} [result] The object on to which the associations will be added. If * no object is passed one is created. This object is then returned. * @param {Boolean/Object} [options] An object containing options describing the data * desired. * @param {Boolean} [options.associated=true] Pass `true` to include associated data from * other associated records. * @param {Boolean} [options.changes=false] Pass `true` to only include fields that * have been modified. Note that field modifications are only tracked for fields that * are not declared with `persist` set to `false`. In other words, only persistent * fields have changes tracked so passing `true` for this means `options.persist` is * redundant. * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`. * This is only meaningful when `options.changes` is `true` since critical fields may * not have been modified. * @param {Boolean} [options.persist] Pass `true` to only return persistent fields. * This is implied when `options.changes` is set to `true`. * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize` * method on the returned fields. * @return {Object} The nested data set for the Model's loaded associations. */ getAssociatedData: function(result, options) { var me = this, associations = me.associations, deep, i, item, items, itemData, length, record, role, roleName, opts, clear, associated; result = result || {}; me.$gathering = 1; if (options) { options = Ext.apply({}, options); } for (roleName in associations) { role = associations[roleName]; item = role.getAssociatedItem(me); if (!item || item.$gathering) { continue; } if (item.isStore) { item.$gathering = 1; items = item.getData().items; // get the records for the store length = items.length; itemData = []; for (i = 0; i < length; ++i) { // NOTE - we don't check whether the record is gathering here because // we cannot remove it from the store (it would invalidate the index // values and misrepresent the content). Instead we tell getData to // only get the fields vs descend further. record = items[i]; deep = !record.$gathering; record.$gathering = 1; if (options) { associated = options.associated; if (associated === undefined) { options.associated = deep; clear = true; } else if (!deep) { options.associated = false; clear = true; } opts = options; } else { opts = deep ? me._getAssociatedOptions : me._getNotAssociatedOptions; } itemData.push(record.getData(opts)); if (clear) { options.associated = associated; clear = false; } delete record.$gathering; } delete item.$gathering; } else { opts = options || me._getAssociatedOptions; if (options && options.associated === undefined) { opts.associated = true; } itemData = item.getData(opts); } result[roleName] = itemData; } delete me.$gathering; return result; }, /** * Gets all values for each field in this model and returns an object containing the * current data. This can be tuned by passing an `options` object with various * properties describing the desired result. Passing `true` simply returns all fields * *and* all associated record data. * * To selectively gather some associated data, the `options` object can be used as * follows: * * var data = order.getData({ * associated: { * orderItems: true * } * }); * * This will include all data fields as well as an "orderItems" array with the data * for each `OrderItem`. To include the associated `Item` for each `OrderItem`, the * call would look like: * * var data = order.getData({ * associated: { * orderItems: { * item: true * } * } * }); * * @param {Boolean/Object} [options] An object containing options describing the data * desired. If `true` is passed it is treated as an object with `associated` set to * `true`. * @param {Boolean/Object} [options.associated=false] Pass `true` to recursively * include all associated data. This is equivalent to pass `true` as the only argument. * See `getAssociatedData`. If `associated` is an object, it describes the specific * associations to gather. * @param {Boolean} [options.changes=false] Pass `true` to only include fields that * have been modified. Note that field modifications are only tracked for fields that * are not declared with `persist` set to `false`. In other words, only persistent * fields have changes tracked so passing `true` for this means `options.persist` is * redundant. * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`. * This is only meaningful when `options.changes` is `true` since critical fields may * not have been modified. * @param {Boolean} [options.persist] Pass `true` to only return persistent fields. * This is implied when `options.changes` is set to `true`. * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize` * method on the returned fields. * @return {Object} An object containing all the values in this model. */ getData: function(options) { var me = this, ret = {}, opts = (options === true) ? me._getAssociatedOptions : (options || ret), // cheat data = me.data, associated = opts.associated, changes = opts.changes, critical = changes && opts.critical, content = changes ? me.modified : data, fieldsMap = me.fieldsMap, persist = opts.persist, serialize = opts.serialize, criticalFields, field, n, name, value; // DON'T use "opts" from here on... // Keep in mind the two legacy use cases: // - getData() ==> Ext.apply({}, me.data) // - getData(true) ==> Ext.apply(Ext.apply({}, me.data), me.getAssociatedData()) if (content) { // when processing only changes, me.modified could be null for (name in content) { value = data[name]; field = fieldsMap[name]; if (field) { if (persist && !field.persist) { continue; } if (serialize && field.serialize) { value = field.serialize(value, me); } } ret[name] = value; } } if (critical) { criticalFields = me.self.criticalFields || me.getCriticalFields(); for (n = criticalFields.length; n-- > 0;) { name = (field = criticalFields[n]).name; if (!(name in ret)) { value = data[name]; if (serialize && field.serialize) { value = field.serialize(value, me); } ret[name] = value; } } } if (associated) { if (typeof associated === 'object') { me.getNestedData(opts, ret); } else { me.getAssociatedData(ret, opts); } } return ret; }, getNestedData: function(options, result) { var me = this, associations = me.associations, graph = options.associated, i, item, items, itemData, length, record, role, roleName, opts; result = result || {}; // For example: // // associated: { // orderItems: true // } // // associated: { // orderItems: { // item: true // } // } // for (roleName in graph) { role = associations[roleName]; opts = graph[roleName]; if (opts === true) { delete options.associated; } else { options.associated = opts; } item = role.getAssociatedItem(me); if (item.isStore) { items = item.getData().items; // get the records for the store length = items.length; itemData = []; for (i = 0; i < length; ++i) { record = items[i]; itemData.push(record.getData(options)); } } else { itemData = item.getData(options); } result[roleName] = itemData; } options.associated = graph; // restore the original value return result; }, /** * Returns the array of fields that are declared as non-persist or "transient". * @return {Ext.data.field.Field[]} * @since 5.0.0 */ getTransientFields: function() { var cls = this.self, ret = cls.transientFields; if (!ret) { cls.rankFields(); // populates transientFields as well as rank ret = cls.transientFields; } return ret; }, /** * Checks whether this model is loading data from the {@link #proxy}. * @return {Boolean} `true` if in a loading state. */ isLoading: function() { return !!this.loadOperation; }, /** * Aborts a pending {@link #method!load} operation. If the record is not loading, this does * nothing. */ abort: function() { var operation = this.loadOperation; if (operation) { operation.abort(); } }, /** * @localdoc Loads the model instance using the configured proxy. The load action * is asynchronous. Any processing of the loaded record should be done in a * callback. * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: [ * {name: 'id', type: 'int'}, * {name: 'name', type: 'string'} * ], * proxy: { * type: 'ajax', * url: 'server.url' * } * }); * * var user = new MyApp.model.User(); * user.load({ * 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, success) { * // do something whether the load succeeded or failed * } * }); * * The options param is an {@link Ext.data.operation.Read} config object containing * success, failure and callback functions, plus optional scope. * * @param {Object} [options] Options to pass to the proxy. * @param {Function} options.success A function to be called when the * model is processed by the proxy successfully. * The callback is passed the following parameters: * @param {Ext.data.Model} options.success.record The record. * @param {Ext.data.operation.Operation} options.success.operation The operation. * * @param {Function} options.failure A function to be called when the * model is unable to be processed by the server. * The callback is passed the following parameters: * @param {Ext.data.Model} options.failure.record The record. * @param {Ext.data.operation.Operation} options.failure.operation The operation. * * @param {Function} options.callback A function to be called whether the proxy * transaction was successful or not. * The callback is passed the following parameters: * @param {Ext.data.Model} options.callback.record The record. * @param {Ext.data.operation.Operation} options.callback.operation The operation. * @param {Boolean} options.callback.success `true` if the operation was successful. * * @param {Object} options.scope The scope in which to execute the callback * functions. Defaults to the model instance. * * @return {Ext.data.operation.Read} The read operation. */ load: function(options) { options = Ext.apply({}, options); /* eslint-disable-next-line vars-on-top */ var me = this, scope = options.scope || me, proxy = me.getProxy(), callback = options.callback, operation = me.loadOperation, id = me.getId(), extras; if (operation) { // Already loading, push any callbacks on and jump out extras = operation.extraCalls; if (!extras) { extras = operation.extraCalls = []; } extras.push(options); return operation; } //<debug> var doIdCheck = true; // eslint-disable-line vars-on-top, one-var if (me.phantom) { doIdCheck = false; } //</debug> options.id = id; // Always set the recordCreator. If we have a session, we're already // part of said session, so we don't need to handle that. options.recordCreator = function(data, type, readOptions) { // Important to change this here, because we might be loading associations, // so we do not want this to propagate down. If we have a session, use that // so that we end up getting the same record. Otherwise, just remove it. var session = me.session; if (readOptions) { readOptions.recordCreator = session ? session.recordCreator : null; } me.set(data, me._commitOptions); //<debug> // Do the id check after set since converters may have run if (doIdCheck && me.getId() !== id) { Ext.raise('Invalid record id returned for ' + id + '@' + me.entityName); } //</debug> return me; }; options.internalCallback = function(operation) { var success = operation.wasSuccessful() && operation.getRecords().length > 0, op = me.loadOperation, extras = op.extraCalls, successFailArgs = [me, operation], callbackArgs = [me, operation, success], i, len; me.loadOperation = null; ++me.loadCount; if (success) { Ext.callback(options.success, scope, successFailArgs); } else { Ext.callback(options.failure, scope, successFailArgs); } Ext.callback(callback, scope, callbackArgs); // Some code repetition here, however in a vast majority of cases // we'll only have a single callback, so optimize for that case rather // than setup arrays for all the callback options if (extras) { for (i = 0, len = extras.length; i < len; ++i) { options = extras[i]; if (success) { Ext.callback(options.success, scope, successFailArgs); } else { Ext.callback(options.failure, scope, successFailArgs); } Ext.callback(options.callback, scope, callbackArgs); } } me.callJoined('afterLoad'); }; delete options.callback; me.loadOperation = operation = proxy.createOperation('read', options); operation.execute(); return operation; }, /** * Merge incoming data from the server when this record exists * in an active session. This method is not called if this record is * loaded directly via {@link #method!load}. The default behaviour is to use incoming * data if the record is not {@link #dirty}, otherwise the data is * discarded. This method should be overridden in subclasses to * provide a different behavior. * @param {Object} data The model data retrieved from the server. * * @protected * * @since 6.5.0 */ mergeData: function(data) { if (!this.dirty) { this.set(data, this._commitOptions); } }, /** * @method save * @localdoc Saves the model instance using the configured proxy. The save action * is asynchronous. Any processing of the saved record should be done in a callback. * * Create example: * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: [ * {name: 'id', type: 'int'}, * {name: 'name', type: 'string'} * ], * proxy: { * type: 'ajax', * url: 'server.url' * } * }); * * var user = new MyApp.model.User({ * name: 'Foo' * }); * * // pass the phantom record data to the server to be saved * user.save({ * failure: function(record, operation) { * // do something if the save failed * }, * success: function(record, operation) { * // do something if the save succeeded * }, * callback: function(record, operation, success) { * // do something whether the save succeeded or failed * } * }); * * The response from a create operation should include the ID for the newly created * record: * * // sample response * { * success: true, * id: 1 * } * * // the id may be nested if the proxy's reader has a rootProperty config * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * proxy: { * type: 'ajax', * url: 'server.url', * reader: { * type: 'ajax', * rootProperty: 'data' * } * } * }); * * // sample nested response * { * success: true, * data: { * id: 1 * } * } * * (Create + ) Update example: * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: [ * {name: 'id', type: 'int'}, * {name: 'name', type: 'string'} * ], * proxy: { * type: 'ajax', * url: 'server.url' * } * }); * * var user = new MyApp.model.User({ * name: 'Foo' * }); * user.save({ * success: function(record, operation) { * record.set('name', 'Bar'); * // updates the remote record via the proxy * record.save(); * } * }); * * (Create + ) Destroy example - see also {@link #erase}: * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: [ * {name: 'id', type: 'int'}, * {name: 'name', type: 'string'} * ], * proxy: { * type: 'ajax', * url: 'server.url' * } * }); * * var user = new MyApp.model.User({ * name: 'Foo' * }); * user.save({ * success: function(record, operation) { * record.drop(); * // destroys the remote record via the proxy * record.save(); * } * }); * * **NOTE:** If a {@link #phantom} record is {@link #drop dropped} and subsequently * saved it will not be processed via the proxy. However, any passed `success` * or `callback` functions will be called. * * The options param is an Operation config object containing success, failure and * callback functions, plus optional scope. The type of Operation depends on the * state of the model being saved. * * - {@link #phantom} model - {@link Ext.data.operation.Create} * - {@link #isModified modified} model - {@link Ext.data.operation.Update} * - {@link #dropped} model - {@link Ext.data.operation.Destroy} * * @inheritdoc #method-load * @return {Ext.data.operation.Create/Ext.data.operation.Update/Ext.data.operation.Destroy} * The operation instance for saving this model. The type of operation returned * depends on the model state at the time of the action. * * - {@link #phantom} model - {@link Ext.data.operation.Create} * - {@link #isModified modified} model - {@link Ext.data.operation.Update} * - {@link #dropped} model - {@link Ext.data.operation.Destroy} */ save: function(options) { options = Ext.apply({}, options); /* eslint-disable-next-line vars-on-top */ var me = this, phantom = me.phantom, dropped = me.