/** * 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, parentRecord; // 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> // Add the parent record to the model instance if a $parentRecordRef exists. // This allows the parent record to be available in a field's convert method. if (data.$parentRecordRef) { parentRecord = data.$parentRecordRef; delete data.$parentRecordRef; // parentRecord[0] -> the inverseName for the association // parentRecord[1] -> the parent record me[parentRecord[0]] = parentRecord[1]; } 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(); result = result || {}; name = field.name; prop = field.summaryField || name; if (name !== 'id') { /* eslint-disable-next-line max-len */ result[name] = summary ? summary.calculate(records, prop, 'data', 0, recLen) : undefined; } } 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 the current associated item is null, move to the next item if (!item) { continue; } 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.dropped, action = dropped ? 'destroy' : (phantom ? 'create' : 'update'), scope = options.scope || me, callback = options.callback, proxy = me.getProxy(), operation; options.records = [me]; options.internalCallback = function(operation) { var args = [me, operation], success = operation.wasSuccessful(); if (success) { Ext.callback(options.success, scope, args); } else { Ext.callback(options.failure, scope, args); } args.push(success); Ext.callback(callback, scope, args); }; delete options.callback; operation = proxy.createOperation(action, options); // Not a phantom, then we must perform this operation on the remote datasource. // Record will be removed from the store in the callback upon a success response if (dropped && phantom) { // If it's a phantom, then call the callback directly with a dummy successful ResultSet operation.setResultSet(Ext.data.reader.Reader.prototype.nullResultSet); me.setErased(); operation.setSuccessful(true); } else { operation.execute(); } return operation; }, //------------------------------------------------------------------------- // Statics statics: { /** * @property {String/Object} * The default proxy to use for instances of this Model when no proxy is configured * on the instance. When specified, the model will use this proxy instead of * requesting one from the {@link Ext.data.Session Session}. * * Can be a string "type", or a {@link Ext.data.proxy.Proxy Proxy} config object. * * This proxy is not inherited by subclasses. * @static * @protected */ defaultProxy: 'memory' }, inheritableStatics: { /** * @property {Object} _associatedReadOptions * The options for the proxy reader for loadData. * * @private */ _associatedReadOptions: { recordsOnly: true, asRoot: true }, /** * Create a model while also parsing any data for associations. * @param {Object} data The model data, including any associated data if required. * The type of data should correspond to what the configured data reader would expect. * @param {Ext.data.Session} [session] The session. * @return {Ext.data.Model} The model. * * @static * @inheritable * @since 6.5.0 */ loadData: function(data, session) { var rec; if (data) { /* eslint-disable-next-line max-len, newline-per-chained-call */ rec = this.getProxy().getReader().readRecords([data], session ? { recordCreator: session.recordCreator } : undefined, this._associatedReadOptions)[0]; } else { rec = new this(data, session); } return rec; }, /** * Get the summary model type. If {@link #summary} is specified, it is * a new type that extends from this type. If not, then it is the same * model type. * @return {Ext.Class} The summary model type. * * @static * @inheritable * @since 6.5.0 */ getSummaryModel: function() { var me = this, proto = me.prototype, summaryModel = me.summaryModel; if (!summaryModel) { summaryModel = Ext.define(null, { extend: me, fields: proto.summaryFields || [], isSummaryModel: true }); summaryModel.isSummaryModel = true; // we do not keep the summary model on the prototype // because the model could be used in different stores // that can have different summaries added dynamically me.summaryModel = summaryModel; } return summaryModel || null; }, /** * Add a new summary field to the model * * @param {String} name Name of the field * @param {Ext.data.summary.Base} summary Name of the summary function */ setSummaryField: function(name, summary) { var field = this.getField(name); if (!field) { this.addFields([{ name: name, type: 'auto', summary: summary, doneSummary: (summary && summary.isAggregator) }]); } else { if (field.doneSummary && summary && summary.isAggregator) { field.summary = summary; } else { field.summary = summary; field.doneSummary = false; } } }, /** * This method adds the given set of fields to this model class. * * @param {String[]/Object[]} newFields The new fields to add. Based on the `name` * of a field this may replace a previous field definition. * * @protected * @static * @inheritable * @since 5.0.0 */ addFields: function(newFields) { this.replaceFields(newFields); }, /** * This method replaces the specified set of fields with a given set of new fields. * Fields should normally be considered immutable, but if the timing is right (that * is, before derived classes are declared), it is permissible to change the fields * collection. * * @param {String[]/Object[]} newFields The new fields to add. Based on the `name` * of a field this may replace a previous field definition. * @param {Boolean/String[]} removeFields The names of fields to remove or `true` * to remove all existing fields. Removes are processed first followed by adds so * if a field name appears in `newFields` as well that field will effectively be * added (however, in that case there is no need to include the field in this * array). * * @protected * @static * @inheritable * @since 5.0.0 */ replaceFields: function(newFields, removeFields) { var me = this, proto = me.prototype, Field = Ext.data.field.Field, fields = me.fields, fieldsMap = me.fieldsMap, ordinals = me.fieldOrdinals, field, i, idField, len, name, ordinal, cleared; if (removeFields === true) { fields.length = 0; me.fieldsMap = fieldsMap = {}; me.fieldOrdinals = ordinals = {}; cleared = true; } else if (removeFields) { for (i = removeFields.length; i-- > 0;) { name = removeFields[i]; if (name in ordinals) { delete ordinals[name]; delete fieldsMap[name]; } } for (i = 0, len = fields.length; i < len; ++i) { name = (field = fields[i]).name; if (name in ordinals) { ordinals[name] = i; } else { // This field is being removed (it is no longer in ordinals). fields.splice(i, 1); --i; --len; // we need to do this forwards so that ordinals don't become // invalid due to a splice } } } for (i = 0, len = newFields ? newFields.length : 0; i < len; i++) { name = (field = newFields[i]).name; if (!(name in ordinals)) { ordinals[name] = ordinal = fields.length; // 0-based fields.push(field = Field.create(field)); fieldsMap[name] = field; field.ordinal = ordinal; field.definedBy = field.owner = this; // Ext.data.NodeInterface } } // Reset all ranks if we didn't get cleared, since this could // alter the dependencies if (!cleared) { for (i = 0, len = fields.length; i < len; ++i) { fields[i].rank = null; } } // The idField could have been replaced, so reacquire it. me.idField = proto.idField = idField = fieldsMap[proto.idProperty]; if (idField) { idField.allowNull = idField.critical = idField.identifier = true; idField.defaultValue = null; } // In case we've created the initializer we need to zap it so we recreate it // next time. Likewise with field ranking. me.initializeFn = me.rankedFields = me.transientFields = me.criticalFields = null; }, /** * Removes the given set of fields from this model. * * @param {Boolean/String[]} removeFields The names of fields to remove or `true` * to remove all existing fields. Removes are processed first followed by adds so * if a field name appears in `newFields` as well that field will effectively be * added (however, in that case there is no need to include the field in this * array). * * @protected * @static * @inheritable * @since 5.0.0 */ removeFields: function(removeFields) { this.replaceFields(null, removeFields); }, /** * @private * @static * @inheritable */ getIdFromData: function(data) { var T = this, idField = T.idField, id = idField.calculated ? (new T(data)).id : data[idField.name]; return id; }, /** * @private * @static * @inheritable */ createWithId: function(id, data, session) { var d = data, T = this; if (id || id === 0) { d = {}; if (data) { Ext.apply(d, data); } d[T.idField.name] = id; } return new T(d, session); }, /** * @private * @static * @inheritable */ getFields: function() { return this.fields; }, /** * @private * @static * @inheritable */ getFieldsMap: function() { return this.fieldsMap; }, /** * @private * @static * @inheritable */ getField: function(name) { return this.fieldsMap[name] || null; }, /** * Returns the configured Proxy for this Model. * @return {Ext.data.proxy.Proxy} The proxy * @static * @inheritable */ getProxy: function() { var me = this, proxy = me.proxy, defaultProxy = me.defaultProxy, defaults; if (!proxy) { // Check what was defined by the class (via onClassExtended): proxy = me.proxyConfig; if (!proxy && defaultProxy) { proxy = defaultProxy; } if (!proxy || !proxy.isProxy) { if (typeof proxy === 'string') { proxy = { type: proxy }; } // We have nothing or a config for the proxy. Get some defaults from // the Schema and smash anything we've provided over the top. defaults = Ext.merge(me.schema.constructProxy(me), proxy); if (proxy && proxy.type) { proxy = proxy.schema === false ? proxy : defaults; } else { proxy = defaults; } } proxy = me.setProxy(proxy); } return proxy; }, /** * Sets the Proxy to use for this model. Accepts any options that can be accepted by * {@link Ext#createByAlias Ext.createByAlias}. * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy * @return {Ext.data.proxy.Proxy} * @static * @inheritable */ setProxy: function(proxy) { var me = this, model; if (proxy) { if (!proxy.isProxy) { proxy = Ext.Factory.proxy(proxy); } else { model = proxy.getModel(); if (model && model !== me) { proxy = proxy.clone(); } } proxy.setModel(me); } return (me.prototype.proxy = me.proxy = proxy); }, /** * Asynchronously loads a model instance by id. Any processing of the loaded * record should be done in a callback. * * Sample usage: * * Ext.define('MyApp.User', { * extend: 'Ext.data.Model', * fields: [ * {name: 'id', type: 'int'}, * {name: 'name', type: 'string'} * ] * }); * * MyApp.User.load(10, { * scope: this, * failure: function(record, operation) { * //do something if the load failed * }, * success: function(record, operation) { * //do something if the load succeeded * }, * callback: function(record, operation, success) { * //do something whether the load succeeded or failed * } * }); * * @param {Number/String} id The ID of the model to load. * **NOTE:** The model returned must have an ID matching the param in the load * request. * * @param {Object} [options] The options param is an * {@link Ext.data.operation.Read} config object containing success, failure and * callback functions, plus optional scope. * * @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. * * @param {Ext.data.Session} [session] The session for this record. * * @return {Ext.data.Model} The newly created model. Note that the model will * (probably) still be loading once it is returned from this method. To do any * post-processing on the data, the appropriate place to do see is in the * callback. * * @static * @inheritable */ load: function(id, options, session) { var data = {}, rec; if (session) { rec = session.peekRecord(this, id); } if (!rec) { data[this.prototype.idProperty] = id; rec = new this(data, session); } rec.load(options); return rec; } }, deprecated: { 5: { methods: { hasId: null, markDirty: null, setDirty: null, eachStore: function(callback, scope) { var me = this, stores = me.stores, len = stores.length, i; for (i = 0; i < len; ++i) { callback.call(scope, stores[i]); } }, join: function(item) { var me = this, stores = me.stores, joined = me.joined; if (!joined) { joined = me.joined = [item]; } else { joined.push(item); } if (item.isStore) { me.store = me.store || item; if (!stores) { stores = me.stores = []; } stores.push(item); } }, unjoin: function(item) { var me = this, stores = me.stores, joined = me.joined; if (joined.length === 1) { joined.length = 0; } else { Ext.Array.remove(joined, item); } if (item.isStore) { Ext.Array.remove(stores, item); me.store = stores[0] || null; } } }, properties: { persistenceProperty: null }, inheritableStatics: { methods: { setFields: null } } } }, //------------------------------------------------------------------------- privates: { _commitOptions: { commit: true }, _getChangesOptions: { changes: true }, _getAssociatedOptions: { associated: true }, _getNotAssociatedOptions: { associated: false }, _metaProperties: { dirty: 'isDirty', phantom: 'isPhantom', valid: 'isValid' }, /** * Copies data from the passed record into this record. If the passed record is undefined, * does nothing. * * If this is a phantom record (represented only in the client, with no corresponding * database entry), and the source record is not a phantom, then this record acquires * the id of the source record. * * @param {Ext.data.Model} sourceRecord The record to copy data from. * @return {String[]} The names of the fields which changed value. * @private */ copyFrom: function(sourceRecord) { var me = this, fields = me.fields, fieldCount = fields.length, modifiedFieldNames = [], idProperty = me.idProperty, i = 0, field, myData, sourceData, name, value; if (sourceRecord) { myData = me.data; sourceData = sourceRecord.data; for (; i < fieldCount; i++) { field = fields[i]; name = field.name; // Do not use setters. // Copy returned values in directly from the data object. // Converters have already been called because new Records // have been created to copy from. // This is a direct record-to-record value copy operation. // don't copy the id, we'll do it at the end if (name !== idProperty) { value = sourceData[name]; // If source property is specified, and value is different // copy field value in and build updatedFields if (value !== undefined && !me.isEqual(myData[name], value)) { myData[name] = value; modifiedFieldNames.push(name); } } } // If this is a phantom record being updated from a concrete record, copy the ID in. if (me.phantom && !sourceRecord.phantom) { // beginEdit to prevent events firing // commit at the end to prevent dirty being set me.beginEdit(); me.setId(sourceRecord.getId()); me.endEdit(true); me.commit(true); } } return modifiedFieldNames; }, /** * Helper function used by afterEdit, afterReject and afterCommit. Calls the given * method on the `Ext.data.Store` that this instance has {@link #join joined}, if any. * The store function will always be called with the model instance as its single * argument. If this model is joined to a Ext.data.NodeStore, then this method calls * the given method on the NodeStore and the associated Ext.data.TreeStore. * @param {String} funcName The name function to call on each store. * @param {Array} [args] The arguments to pass to the method. This instance is * always inserted as the first argument. * @private */ callJoined: function(funcName, args) { var me = this, joined = me.joined, session = me.session, state = me.dropped ? 'D' : (me.phantom ? 'C' : (me.dirty ? 'U' : 'R')), i, len, fn, item; me.crudState = state; if (joined || session) { if (args) { args.unshift(me); } else { args = [me]; } fn = session && session[funcName]; if (fn) { fn.apply(session, args); } if (joined) { for (i = 0, len = joined.length; i < len; ++i) { item = joined[i]; if (item && (fn = item[funcName])) { fn.apply(item, args); } } } } me.crudStateWas = state; }, /** * Currently this only checks the loading state, this method exists for API * parity with stores. * @return {Boolean} `true` if the model is loading or has a pending load. * * @private */ hasPendingLoad: function() { return this.isLoading(); }, interpret: function(name) { var me = this, accessor = me._metaProperties[name]; if (!accessor) { accessor = me.associations; // e.g. "orderItems" accessor = accessor && accessor[name] && accessor[name].getterName; } if (accessor) { return me[accessor](); // e.g., me.isPhantom() } return me.data[name]; }, /** * Gets the dirty state of this record. * @return {Boolean} The dirty state. * * @private */ isDirty: function() { // Added as a method to be used by data binding return this.dirty; }, /** * Gets the phantom state of this record. * @return {Boolean} The phantom state. * * @private */ isPhantom: function() { // Added as a method to be used by data binding return this.phantom; }, /** * Called when an associated record instance has been set. * @param {Ext.data.Model} record The record. * @param {Ext.data.schema.Role} role The role. * * @private */ onAssociatedRecordSet: function(record, role) { this.callJoined('afterAssociatedRecordSet', [record, role]); }, /** * @method * Called when the model id is changed. * @param {Object} id The new id. * @param {Object} oldId The old id. */ onIdChanged: Ext.privateFn, /** * Set the session for this record. * @param {Ext.data.Session} session The session */ setSession: function(session) { //<debug> if (session) { if (this.session) { Ext.raise('This model already belongs to a session.'); } if (!this.id) { Ext.raise('The model must have an id to participate in a session.'); } } //</debug> this.session = session; if (session) { session.add(this); } }, /** * Gets the names of all the fields that were modified during an edit. * @param {Object} [old] The saved data from `beginEdit`. * @return {String[]} The array of modified field names. * @private */ getModifiedFieldNames: function(old) { var me = this, data = me.data, modified = [], oldData = old || me.editMemento.data, key; for (key in data) { if (data.hasOwnProperty(key)) { if (!me.isEqual(data[key], oldData[key], key)) { modified.push(key); } } } return modified; }, /** * Checks if two values are equal, taking into account certain special factors, for * example dates. * @param {Object} lhs The first value. * @param {Object} rhs The second value. * @param {String/Ext.data.Field} [field] The field name or instance. * @return {Boolean} True if the values are equal. * @private */ isEqual: function(lhs, rhs, field) { var f; if (field) { f = field.isField ? field : this.fieldsMap[field]; if (f) { return f.isEqual(lhs, rhs); } } // instanceof is ~10 times faster then Ext.isDate. Values here will not be // cross-document objects if (lhs instanceof Date && rhs instanceof Date) { return lhs.getTime() === rhs.getTime(); } return lhs === rhs; }, statics: { /** * @property * @static * @private * @readonly * @deprecated 5.0 Use the string `"edit"` directly. * The update operation of type 'edit'. Used by the * {@link Ext.data.Store#event-update Store.update} event. */ EDIT: 'edit', /** * @property * @static * @private * @readonly * @deprecated 5.0 Use the string `"reject"` directly. * The update operation of type 'reject'. Used by the * {@link Ext.data.Store#event-update Store.update} event. */ REJECT: 'reject', /** * @property * @static * @private * @readonly * @deprecated 5.0 Use the string `"commit"` directly. * The update operation of type 'commit'. Used by the * {@link Ext.data.Store#event-update Store.update} event. */ COMMIT: 'commit', rankFields: function() { var cls = this, prototype = cls.prototype, fields = cls.fields, length = fields.length, rankedFields = [], criticalFields = [], transientFields = [], evilFields, field, i; cls.rankedFields = prototype.rankedFields = rankedFields; cls.criticalFields = prototype.criticalFields = criticalFields; cls.transientFields = prototype.transientFields = transientFields; // This first pass brings over any fields that have no dependencies at all // and gathers the evil fields to the side (the fields that could depend on // anything). This avoids the call to topoAdd that we must perform on all of // the fields that do have depends (which is good since most fields will be // handled here). for (i = 0; i < length; ++i) { field = fields[i]; if (field.critical) { criticalFields.push(field); } if (!field.persist) { transientFields.push(field); } if (field.evil) { (evilFields || (evilFields = [])).push(field); } else if (!field.depends) { rankedFields.push(field); field.rank = rankedFields.length; // 1-based } } for (i = 0; i < length; ++i) { if (!(field = fields[i]).rank && !field.evil) { cls.topoAdd(field); } } if (evilFields) { for (i = 0, length = evilFields.length; i < length; ++i) { rankedFields.push(field = evilFields[i]); field.rank = rankedFields.length; // 1-based } } //<debug> cls.topoStack = null; // cleanup diagnostic stack //</debug> return rankedFields; }, topoAdd: function(field) { var cls = this, dep = field.depends, dependsLength = dep ? dep.length : 0, rankedFields = cls.rankedFields, i, targetField; //<debug> /* eslint-disable-next-line vars-on-top, one-var */ var topoStack = cls.topoStack || (cls.topoStack = []); topoStack.push(field.name); if (field.rank === 0) { // if (adding) Ext.raise(cls.$className + " has circular field dependencies: " + topoStack.join(" --> ")); } if (topoStack.length && field.evil) { Ext.raise(cls.$className + ": Field " + topoStack[topoStack.length - 1] + " cannot depend on depends-less field " + field.name); } field.rank = 0; // adding (falsey but we can still detect cycles) //</debug> for (i = 0; i < dependsLength; ++i) { // Get the targetField on which we depend and add this field to the // targetField.dependents[] targetField = cls.fieldsMap[dep[i]]; //<debug> if (!targetField) { Ext.raise(cls.$className + ": Field " + field.name + " depends on undefined field " + dep[i]); } //</debug> (targetField.dependents || (targetField.dependents = [])).push(field); if (!targetField.rank) { // if (!added) cls.topoAdd(targetField); } } rankedFields.push(field); field.rank = rankedFields.length; // 1-based (truthy to track "added" state) //<debug> topoStack.pop(); //</debug> }, initFields: function(data, cls, proto) { var Field = Ext.data.field.Field, fieldDefs = data.fields, // allocate fields [] and ordinals {} for the new class: fields = [], fieldOrdinals = {}, fieldsMap = {}, references = [], superFields = proto.fields, versionProperty = data.versionProperty || proto.versionProperty, idProperty = cls.idProperty, idField, field, i, length, name, ordinal, reference, superIdField, superIdFieldName, superIdDeclared, idDeclared; // Process any inherited fields to produce a fields [] and ordinals {} for // this class: cls.fields = proto.fields = fields; cls.fieldOrdinals = proto.fieldOrdinals = fieldOrdinals; cls.fieldsMap = proto.fieldsMap = fieldsMap; cls.references = proto.references = references; if (superFields) { // We chain the super field so we can write to it for (i = 0, length = superFields.length; i < length; ++i) { fields[i] = field = Ext.Object.chain(superFields[i]); field.dependents = null; // we need to recalculate these field.owner = cls; fieldOrdinals[name = field.name] = i; fieldsMap[name] = field; // Clear the rank because it needs to be set on the first pass through // the fields in the subclass, don't inherit it from the parent field.rank = null; if (field.generated) { superIdField = field; superIdFieldName = field.name; } } } // Merge in any fields from this class: delete data.fields; if (fieldDefs) { for (i = 0, length = fieldDefs.length; i < length; ++i) { field = fieldDefs[i]; reference = field.reference; // Create a copy of the reference since we'll modify // the reference on the field. Needed for subclasses if (reference && typeof reference !== 'string') { // Can have child objects, so merge it deeply reference = Ext.merge({}, reference); } field.$reference = reference; field = Field.create(fieldDefs[i]); name = field.name; ordinal = fieldOrdinals[name]; if (ordinal === undefined) { // If the field is new, add it to the end of the fields[] fieldOrdinals[name] = ordinal = fields.length; } // else, overwrite the field at the established ordinal fieldsMap[name] = field; fields[ordinal] = field; field.definedBy = field.owner = cls; field.ordinal = ordinal; if (name === idProperty) { idDeclared = field; } if (name === superIdFieldName) { superIdDeclared = true; } } } // Lookup the idProperty in the ordinals map and create a synthetic field if // we don't have one. idField = fieldsMap[idProperty]; if (!idField) { if (superIdField && superIdField.generated) { ordinal = superIdField.ordinal; } else { ordinal = fields.length; } delete fieldsMap[superIdFieldName]; delete fieldOrdinals[superIdFieldName]; idField = new Field(idProperty); fields[ordinal] = idField; fieldOrdinals[idProperty] = ordinal; fieldsMap[idProperty] = idField; idField.definedBy = cls; idField.ordinal = ordinal; idField.generated = true; } else if (idDeclared && !superIdDeclared && superIdField && superIdField.generated) { // If we're declaring the id as a field in our fields array and it's different // to the super id field that has been generated, pull it out and fix up // the ordinals. This likely won't happen often, to do it earlier we would need // to know the contents of the fields which would mean iterating over them // twice. Ext.Array.remove(fields, superIdField); delete fieldsMap[superIdFieldName]; delete fieldOrdinals[superIdFieldName]; fieldsMap[idProperty] = idDeclared; for (i = 0, length = fields.length; i < length; ++i) { field = fields[i]; fields.ordinal = i; fieldOrdinals[field.name] = i; } } idField.allowNull = idField.critical = idField.identifier = true; idField.defaultValue = null; cls.idField = proto.idField = idField; if (versionProperty) { field = fieldsMap[versionProperty]; if (!field) { ordinal = fields.length; field = new Field({ name: versionProperty, type: 'int' }); fields[ordinal] = field; fieldOrdinals[versionProperty] = ordinal; fieldsMap[versionProperty] = field; field.definedBy = cls; field.ordinal = ordinal; field.generated = true; } field.defaultValue = 1; field.critical = true; } // NOTE: Be aware that the one fellow that manipulates these after this // point is Ext.data.NodeInterface. }, initSummaries: function(data, cls, proto) { var summaryDefs = data.summary, fields = proto.fields, superSummaryFields = proto.summaryFields, field, fieldName, i, index, inlineDefs, len, name, summaryFields, summaryFieldMap, summaryField, type; if (superSummaryFields) { summaryFields = []; summaryFieldMap = {}; for (i = 0, len = superSummaryFields.length; i < len; ++i) { summaryField = superSummaryFields[i]; summaryFields.push(summaryField); summaryFieldMap[summaryField.name] = i; } } // We take any "summary: foo" configs on the model fields (but not ones // that were inherited since those come in above) and reform them into an // equivalent "summary: {}" as one would define on the class body. for (i = 0, len = fields.length; i < len; ++i) { field = fields[i]; if (field.summary && field.definedBy === cls) { inlineDefs = inlineDefs || {}; inlineDefs[field.name] = field.summary; } } // If they've got inline summary configs on the fields, merge them with // whatever is on the class body. The "summary" on the class body wins // when there is a collision. We don't worry about collisions because it // is a common need to use code generation to define model fields, so we // allow the class summary definition to override whatever is on them. if (inlineDefs) { summaryDefs = Ext.apply(inlineDefs, summaryDefs); } if (summaryDefs) { delete data.summary; summaryFields = summaryFields || []; summaryFieldMap = summaryFieldMap || {}; for (name in summaryDefs) { type = typeof(summaryField = summaryDefs[name]); if (type === 'function' || type === 'string') { summaryField = { summary: summaryField }; } // If it's not in the summaries, it's new here. We've already // applied when copying down so this is safe to do index = summaryFieldMap[name]; summaryField = Ext.apply({ name: name }, summaryField); field = summaryField.field; if (field) { delete summaryField.field; summaryField.summaryField = field; } fieldName = summaryField.summaryField; // Tempting to do the following, but we do not require that all // fields be defined: // // if (fieldName && !proto.fieldsMap[fieldName]) { // Ext.raise('No field named "' + fieldName + '" on ' + ( // proto.hasOwnProperty('$className') // ? '"' + proto.$className + '"' // : 'anonymous model' // )); // } field = proto.fieldsMap[fieldName || name]; if (field && !summaryField.type) { summaryField.type = field.type; } if (index === undefined) { index = summaryFields.length; summaryFieldMap[name] = index; } summaryFields[index] = summaryField; } } // Store these in an array so we have a predictable order when subclassing if (summaryFields) { proto.summaryFields = summaryFields; } }, initValidators: function(data, cls, proto) { var superValidators = proto.validators, validators, field, copy, validatorDefs, i, length, fieldValidator, name, validator, item; if (superValidators) { validators = {}; for (field in superValidators) { validators[field] = Ext.Array.clone(superValidators[field]); } } validatorDefs = data.validators || data.validations; //<debug> if (data.validations) { delete data.validations; Ext.log.warn((cls.$className || 'Ext.data.Model') + ': validations has been deprecated. Please use validators ' + 'instead.'); } //</debug> if (validatorDefs) { delete data.validators; validators = validators || {}; // Support older array syntax if (Ext.isArray(validatorDefs)) { copy = {}; for (i = 0, length = validatorDefs.length; i < length; ++i) { item = validatorDefs[i]; name = item.field; if (!copy[name]) { copy[name] = []; } // Check for function form item = item.fn || item; copy[name].push(item); } validatorDefs = copy; } for (name in validatorDefs) { fieldValidator = validatorDefs[name]; if (!Ext.isArray(fieldValidator)) { fieldValidator = [fieldValidator]; } validator = validators[name]; if (validator) { // Declared in super Ext.Array.push(validator, fieldValidator); } else { validators[name] = fieldValidator; } } } if (validators) { for (name in validators) { field = cls.getField(name); if (field) { field.setModelValidators(validators[name]); } } } cls.validators = proto.validators = validators; }, initAssociations: function(schema, data, cls) { // Handle keyless associations var associations = data.associations, belongsTo = data.belongsTo, hasMany = data.hasMany, hasOne = data.hasOne, // manyToMany can't be declared via reference matrices = data.manyToMany, i, length, assoc, o; delete data.associations; delete data.belongsTo; delete data.hasMany; delete data.hasOne; delete data.manyToMany; if (matrices) { schema.addMatrices(cls, matrices); } if (associations) { associations = Ext.isArray(associations) ? associations : [ associations ]; for (i = 0, length = associations.length; i < length; ++i) { assoc = associations[i]; o = Ext.apply({}, assoc); delete o.type; switch (assoc.type) { case 'belongsTo': schema.addBelongsTo(cls, o); break; case 'hasMany': schema.addHasMany(cls, o); break; case 'hasOne': schema.addHasOne(cls, o); break; //<debug> default: Ext.raise('Invalid association type: "' + assoc.type + '"'); //</debug> } } } if (belongsTo) { belongsTo = Ext.isArray(belongsTo) ? belongsTo : [ belongsTo ]; for (i = 0, length = belongsTo.length; i < length; ++i) { schema.addBelongsTo(cls, belongsTo[i]); } } if (hasMany) { hasMany = Ext.isArray(hasMany) ? hasMany : [ hasMany ]; for (i = 0, length = hasMany.length; i < length; ++i) { schema.addHasMany(cls, hasMany[i]); } } if (hasOne) { hasOne = Ext.isArray(hasOne) ? hasOne : [ hasOne ]; for (i = 0, length = hasOne.length; i < length; ++i) { schema.addHasOne(cls, hasOne[i]); } } schema.afterKeylessAssociations(cls); }, initIdentifier: function(data, cls, proto) { var identifier = data.identifier || data.idgen, superIdent = proto.identifier || cls.schema._defaultIdentifier, generatorPrefix; //<debug> if (data.idgen) { Ext.log.warn('Ext.data.Model: idgen has been deprecated. Please use ' + 'identifier instead.'); } //</debug> if (identifier) { delete data.identifier; delete data.idgen; // An idgen was specified on the definition, use it explicitly. identifier = Ext.Factory.dataIdentifier(identifier); } else if (superIdent) { // If we have a cloneable instance, and we don't have an id // clone it. If we have an id, then we should use the same // instance since it's the same as looking it up via id. if (superIdent.clone && !superIdent.getId()) { identifier = superIdent.clone(); } else if (superIdent.isGenerator) { identifier = superIdent; } else { identifier = Ext.Factory.dataIdentifier(superIdent); } } cls.identifier = proto.identifier = identifier; if (!identifier) { // If we didn't find one, create it and push it onto the class. // Don't put it on the prototype, so a subclass will create // it's own generator. If we have an anonymous model, go ahead and // generate a unique prefix for it. generatorPrefix = cls.entityName; if (!generatorPrefix) { generatorPrefix = Ext.id(null, 'extModel'); } cls.identifier = Ext.Factory.dataIdentifier({ type: 'sequential', prefix: generatorPrefix + '-' }); } }, findValidator: function(validators, name, cfg) { var type = cfg.type || cfg, field = validators[name], len, i, item; if (field) { for (i = 0, len = field.length; i < len; ++i) { item = field[i]; if (item.type === type) { return item; } } } return null; }, /** * This method produces the `initializeFn` for this class. If there are no fields * requiring {@link Ext.data.field.Field#cfg-convert conversion} and no fields requiring * a {@link Ext.data.field.Field#defaultValue default value} then this method will * return `null`. * @return {Function} The `initializeFn` for this class (or null). * @private */ makeInitializeFn: function(cls) { var code = ['var '], body = ['\nreturn function (e) {\n var data = e.data, v;\n'], work = 0, bc, ec, // == beginClone, endClone convert, expr, factory, field, fields, fs, hasDefValue, i, length; if (!(fields = cls.rankedFields)) { // On the first edit of a record of this type we need to ensure we have the // topo-sort done: fields = cls.rankFields(); } for (i = 0, length = fields.length; i < length; ++i) { // The generated method declares vars for each field using "f0".."fN' as the // name. These are used to access properties of the field (e.g., the convert // method or defaultValue). field = fields[i]; fs = 'f' + i; convert = field.convert; if (i) { code.push(', \n '); } code.push(fs, ' = $fields[' + i + ']'); //<debug> // this can be helpful when debugging (at least in Chrome): code.push(' /* ', field.name, ' */'); //</debug> // NOTE: added string literals are "folded" by the compiler so we // are better off doing an "'foo' + 'bar'" then "'foo', 'bar'". But // for variables we are better off pushing them into the array for // the final join. if ((hasDefValue = (field.defaultValue !== undefined)) || convert) { // For non-calculated fields that have some work required (a convert method // and/or defaultValue), generate a chunk of logic appropriate for the // field. // expr = data["fieldName"]; expr = 'data["' + field.name + '"]'; ++work; bc = ec = ''; if (field.cloneDefaultValue) { bc = 'Ext.clone('; ec = ')'; } body.push('\n'); if (convert && hasDefValue) { // v = data.fieldName; // if (v !== undefined) { // v = f2.convert(v, e); // } // if (v === undefined) { // v = f2.defaultValue; // // or // v = Ext.clone(f2.defaultValue); // } // data.fieldName = v; // body.push(' v = ', expr, ';\n' + ' if (v !== undefined) {\n' + ' v = ', fs, '.convert(v, e);\n' + ' }\n' + ' if (v === undefined) {\n' + ' v = ', bc, fs, '.defaultValue', ec, ';\n' + ' }\n' + ' ', expr, ' = v;'); } else if (convert) { // no defaultValue // v = f2.convert(data.fieldName,e); // if (v !== undefined) { // data.fieldName = v; // } // body.push(' v = ', fs, '.convert(', expr, ',e);\n' + ' if (v !== undefined) {\n' + ' ', expr, ' = v;\n' + ' }\n'); } else if (hasDefValue) { // no convert // if (data.fieldName === undefined) { // data.fieldName = f2.defaultValue; // // or // data.fieldName = Ext.clone(f2.defaultValue); // } // body.push(' if (', expr, ' === undefined) {\n' + ' ', expr, ' = ', bc, fs, '.defaultValue', ec, ';\n' + ' }\n'); } } } if (!work) { // There are no fields that need special processing return Ext.emptyFn; } code.push(';\n'); code.push.apply(code, body); code.push('}'); code = code.join(''); // Ensure that Ext in the function code refers to the same Ext that we are // using here. If we are in a sandbox, global.Ext might be different. factory = new Function('$fields', 'Ext', code); return factory(fields, Ext); } } // static } // privates},/* eslint-disable indent */function() { var Model = this, proto = Model.prototype, Schema = Ext.data.schema.Schema, defaultSchema; Model.proxyConfig = proto.proxy; delete proto.proxy; // Base Model class may be used. It needs an empty fields array. Model.fields = []; // Base Model class may be used. It needs an empty fieldsMap hash. Model.fieldsMap = proto.fieldsMap = {}; Model.schema = proto.schema = Schema.get(proto.schema); proto.idField = new Ext.data.field.Field(proto.idProperty); Model.identifier = new Ext.data.identifier.Sequential(); Model.onExtended(function(cls, data) { var proto = cls.prototype, schemaName = data.schema, superCls = proto.superclass.self, schema, entityName, proxy; cls.idProperty = data.idProperty || proto.idProperty; if (schemaName) { delete data.schema; schema = Schema.get(schemaName); } else if (!(schema = proto.schema)) { schema = defaultSchema || (defaultSchema = Schema.get('default')); } // These are in "privates" so we manually make them inherited: cls.rankFields = Model.rankFields; cls.topoAdd = Model.topoAdd; // if we picked up a schema from cls.prototype.schema, it is because it was found // in the prototype chain on a base class. proto.schema = cls.schema = schema; // Unless specified on the declaration data, we need to provide the entityName of // the new Entity-derived class. Store it on the prototype and the class. if (!(entityName = data.entityName)) { proto.entityName = entityName = schema.getEntityName(cls); //<debug> if (!entityName) { if (data.associations) { Ext.raise('Anonymous entities cannot specify "associations"'); } if (data.belongsTo) { Ext.raise('Anonymous entities cannot specify "belongsTo"'); } if (data.hasMany) { Ext.raise('Anonymous entities cannot specify "hasMany"'); } if (data.hasOne) { Ext.raise('Anonymous entities cannot specify "hasOne"'); } if (data.matrices) { Ext.raise('Anonymous entities cannot specify "manyToMany"'); } } //</debug> } cls.entityName = entityName; cls.fieldExtractors = {}; Model.initIdentifier(data, cls, proto); Model.initFields(data, cls, proto); Model.initValidators(data, cls, proto); if (!data.isSummaryModel) { Model.initSummaries(data, cls, proto); } // This is a compat hack to allow "rec.fields.items" to work as it used to when // fields was a MixedCollection cls.fields.items = cls.fields; if (entityName) { schema.addEntity(cls); Model.initAssociations(schema, data, cls); } proxy = data.proxy; if (proxy) { delete data.proxy; } else if (superCls !== Model) { proxy = superCls.proxyConfig || superCls.proxy; } cls.proxyConfig = proxy; });});