X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/7a654f8d43fdb43d78b63d90528bed6e86b608cc..f562e4c6e5fac7bcb445985b99acbea4d706e6f0:/src/data/Model.js?ds=sidebyside diff --git a/src/data/Model.js b/src/data/Model.js index acb302aa..2df497b1 100644 --- a/src/data/Model.js +++ b/src/data/Model.js @@ -1,235 +1,230 @@ +/* + +This file is part of Ext JS 4 + +Copyright (c) 2011 Sencha Inc + +Contact: http://www.sencha.com/contact + +GNU General Public License Usage +This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html. + +If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact. + +*/ /** * @author Ed Spencer - * @class Ext.data.Model * - *

A Model represents some object that your application manages. For example, one might define a Model for Users, Products, - * Cars, or any other real-world object that we want to model in the system. Models are registered via the {@link Ext.ModelManager model manager}, - * and are used by {@link Ext.data.Store stores}, which are in turn used by many of the data-bound components in Ext.

+ * A Model represents some object that your application manages. For example, one might define a Model for Users, + * Products, Cars, or any other real-world object that we want to model in the system. Models are registered via the + * {@link Ext.ModelManager model manager}, and are used by {@link Ext.data.Store stores}, which are in turn used by many + * of the data-bound components in Ext. * - *

Models are defined as a set of fields and any arbitrary methods and properties relevant to the model. For example:

+ * 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'},
-        {name: 'phone', type: 'string'},
-        {name: 'alive', type: 'boolean', defaultValue: true}
-    ],
-
-    changeName: function() {
-        var oldName = this.get('name'),
-            newName = oldName + " The Barbarian";
-
-        this.set('name', newName);
-    }
-});
-
-* -*

The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link Ext.ModelManager ModelManager}, and all -* other functions and properties are copied to the new Model's prototype.

-* -*

Now we can create instances of our User model and call any model logic we defined:

-* -

-var user = Ext.ModelManager.create({
-    name : 'Conan',
-    age  : 24,
-    phone: '555-555-5555'
-}, 'User');
-
-user.changeName();
-user.get('name'); //returns "Conan The Barbarian"
-
+ * Ext.define('User', { + * extend: 'Ext.data.Model', + * fields: [ + * {name: 'name', type: 'string'}, + * {name: 'age', type: 'int'}, + * {name: 'phone', type: 'string'}, + * {name: 'alive', type: 'boolean', defaultValue: true} + * ], * - *

Validations

+ * changeName: function() { + * var oldName = this.get('name'), + * newName = oldName + " The Barbarian"; * - *

Models have built-in support for validations, which are executed against the validator functions in - * {@link Ext.data.validations} ({@link Ext.data.validations see all validation functions}). Validations are easy to add to models:

+ * this.set('name', newName); + * } + * }); * -

-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}
-    ],
-
-    validations: [
-        {type: 'presence',  field: 'age'},
-        {type: 'length',    field: 'name',     min: 2},
-        {type: 'inclusion', field: 'gender',   list: ['Male', 'Female']},
-        {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']},
-        {type: 'format',    field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}
-    ]
-});
-
+ * The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link + * Ext.ModelManager ModelManager}, and all other functions and properties are copied to the new Model's prototype. * - *

The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors} - * object:

+ * Now we can create instances of our User model and call any model logic we defined: * -

-var instance = Ext.ModelManager.create({
-    name: 'Ed',
-    gender: 'Male',
-    username: 'edspencer'
-}, 'User');
-
-var errors = instance.validate();
-
+ * var user = Ext.create('User', { + * name : 'Conan', + * age : 24, + * phone: '555-555-5555' + * }); * - *

Associations

+ * user.changeName(); + * user.get('name'); //returns "Conan The Barbarian" * - *

Models can have associations with other Models via {@link Ext.data.BelongsToAssociation belongsTo} and - * {@link Ext.data.HasManyAssociation hasMany} associations. For example, let's say we're writing a blog administration - * application which deals with Users, Posts and Comments. We can express the relationships between these models like this:

+ * # Validations * -

-Ext.define('Post', {
-    extend: 'Ext.data.Model',
-    fields: ['id', 'user_id'],
-
-    belongsTo: 'User',
-    hasMany  : {model: 'Comment', name: 'comments'}
-});
-
-Ext.define('Comment', {
-    extend: 'Ext.data.Model',
-    fields: ['id', 'user_id', 'post_id'],
-
-    belongsTo: 'Post'
-});
-
-Ext.define('User', {
-    extend: 'Ext.data.Model',
-    fields: ['id'],
-
-    hasMany: [
-        'Post',
-        {model: 'Comment', name: 'comments'}
-    ]
-});
-
+ * Models have built-in support for validations, which are executed against the validator functions in {@link + * Ext.data.validations} ({@link Ext.data.validations see all validation functions}). Validations are easy to add to + * models: * - *

See the docs for {@link Ext.data.BelongsToAssociation} and {@link Ext.data.HasManyAssociation} for details on the usage - * and configuration of associations. Note that associations can also be specified like this:

+ * Ext.define('User', { + * extend: 'Ext.data.Model', + * 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} + * ], * -

-Ext.define('User', {
-    extend: 'Ext.data.Model',
-    fields: ['id'],
-
-    associations: [
-        {type: 'hasMany', model: 'Post',    name: 'posts'},
-        {type: 'hasMany', model: 'Comment', name: 'comments'}
-    ]
-});
-
+ * validations: [ + * {type: 'presence', field: 'age'}, + * {type: 'length', field: 'name', min: 2}, + * {type: 'inclusion', field: 'gender', list: ['Male', 'Female']}, + * {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']}, + * {type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/} + * ] + * }); * - *

Using a Proxy

+ * The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors} + * object: * - *

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:

+ * var instance = Ext.create('User', { + * name: 'Ed', + * gender: 'Male', + * username: 'edspencer' + * }); * -

-Ext.define('User', {
-    extend: 'Ext.data.Model',
-    fields: ['id', 'name', 'email'],
-
-    proxy: {
-        type: 'rest',
-        url : '/users'
-    }
-});
-
+ * var errors = instance.validate(); * - *

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:

+ * # Associations * -

-var user = Ext.ModelManager.create({name: 'Ed Spencer', email: 'ed@sencha.com'}, 'User');
-
-user.save(); //POST /users
-
+ * Models can have associations with other Models via {@link Ext.data.BelongsToAssociation belongsTo} and {@link + * Ext.data.HasManyAssociation hasMany} associations. For example, let's say we're writing a blog administration + * application which deals with Users, Posts and Comments. We can express the relationships between these models like + * this: * - *

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.

+ * Ext.define('Post', { + * extend: 'Ext.data.Model', + * fields: ['id', 'user_id'], * - *

Loading data via the Proxy is equally easy:

+ * belongsTo: 'User', + * hasMany : {model: 'Comment', name: 'comments'} + * }); * -

-//get a reference to the User model class
-var User = Ext.ModelManager.getModel('User');
-
-//Uses the configured RestProxy to make a GET request to /users/123
-User.load(123, {
-    success: function(user) {
-        console.log(user.getId()); //logs 123
-    }
-});
-
+ * Ext.define('Comment', { + * extend: 'Ext.data.Model', + * fields: ['id', 'user_id', 'post_id'], * - *

Models can also be updated and destroyed easily:

+ * belongsTo: 'Post' + * }); * -

-//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.destroy({
-    success: function() {
-        console.log('The User was destroyed!');
-    }
-});
-
+ * Ext.define('User', { + * extend: 'Ext.data.Model', + * fields: ['id'], * - *

Usage in Stores

+ * hasMany: [ + * 'Post', + * {model: 'Comment', name: 'comments'} + * ] + * }); * - *

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}:

+ * See the docs for {@link Ext.data.BelongsToAssociation} and {@link Ext.data.HasManyAssociation} for details on the + * usage and configuration of associations. Note that associations can also be specified like this: * -

-var store = new Ext.data.Store({
-    model: 'User'
-});
-
-//uses the Proxy we set up on Model to load the Store data
-store.load();
-
+ * Ext.define('User', { + * extend: 'Ext.data.Model', + * fields: ['id'], + * + * associations: [ + * {type: 'hasMany', model: 'Post', name: 'posts'}, + * {type: 'hasMany', model: 'Comment', name: 'comments'} + * ] + * }); + * + * # Using a Proxy + * + * Models are great for representing types of data and relationships, but sooner or later we're going to want to load or + * save that data somewhere. All loading and saving of data is handled via a {@link Ext.data.proxy.Proxy Proxy}, which + * can be set directly on the Model: + * + * Ext.define('User', { + * extend: 'Ext.data.Model', + * fields: ['id', 'name', 'email'], * - *

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.

+ * proxy: { + * type: 'rest', + * url : '/users' + * } + * }); + * + * Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save data to and from a + * RESTful backend. Let's see how this works: + * + * var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'}); + * + * user.save(); //POST /users + * + * Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to persist this Model's + * data onto our server. RestProxy figures out that this Model hasn't been saved before because it doesn't have an id, + * and performs the appropriate action - in this case issuing a POST request to the url we configured (/users). We + * configure any Proxy on any Model and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list. + * + * Loading data via the Proxy is equally easy: + * + * //get a reference to the User model class + * var User = Ext.ModelManager.getModel('User'); + * + * //Uses the configured RestProxy to make a GET request to /users/123 + * User.load(123, { + * success: function(user) { + * console.log(user.getId()); //logs 123 + * } + * }); + * + * Models can also be updated and destroyed easily: + * + * //the user Model we loaded in the last snippet: + * user.set('name', 'Edward Spencer'); + * + * //tells the Proxy to save the Model. In this case it will perform a PUT request to /users/123 as this Model already has an id + * user.save({ + * success: function() { + * console.log('The User was updated'); + * } + * }); + * + * //tells the Proxy to destroy the Model. Performs a DELETE request to /users/123 + * user.destroy({ + * success: function() { + * console.log('The User was destroyed!'); + * } + * }); + * + * # Usage in Stores + * + * It is very common to want to load a set of Model instances to be displayed and manipulated in the UI. We do this by + * creating a {@link Ext.data.Store Store}: + * + * var store = Ext.create('Ext.data.Store', { + * model: 'User' + * }); + * + * //uses the Proxy we set up on Model to load the Store data + * store.load(); + * + * A Store is just a collection of Model instances - usually loaded from a server somewhere. Store can also maintain a + * set of added, updated and removed Model instances to be synchronized with the server via the Proxy. See the {@link + * Ext.data.Store Store docs} for more information on Stores. * * @constructor + * Creates new Model instance. * @param {Object} data An object containing keys corresponding to this model's fields, and their associated values - * @param {Number} id Optional unique ID to assign to this model instance + * @param {Number} id (optional) Unique ID to assign to this model instance */ Ext.define('Ext.data.Model', { alternateClassName: 'Ext.data.Record', - + mixins: { observable: 'Ext.util.Observable' }, requires: [ 'Ext.ModelManager', + 'Ext.data.IdGenerator', 'Ext.data.Field', 'Ext.data.Errors', 'Ext.data.Operation', @@ -252,6 +247,7 @@ Ext.define('Ext.data.Model', { associations = data.associations || [], belongsTo = data.belongsTo, hasMany = data.hasMany, + idgen = data.idgen, fieldsMixedCollection = new Ext.util.MixedCollection(false, function(field) { return field.name; @@ -290,6 +286,10 @@ Ext.define('Ext.data.Model', { data.fields = fieldsMixedCollection; + if (idgen) { + data.idgen = Ext.data.IdGenerator.get(idgen); + } + //associations can be specified in the more convenient format (e.g. not inside an 'associations' array). //we support that here if (belongsTo) { @@ -369,14 +369,17 @@ Ext.define('Ext.data.Model', { // Fire the onModelDefined template method on ModelManager Ext.ModelManager.onModelDefined(cls); }); - } + }; }, inheritableStatics: { /** - * Sets the Proxy to use for this model. Accepts any options that can be accepted by {@link Ext#createByAlias Ext.createByAlias} + * 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) { //make sure we have an Ext.data.proxy.Proxy object @@ -397,40 +400,42 @@ Ext.define('Ext.data.Model', { /** * Returns the configured Proxy for this Model * @return {Ext.data.proxy.Proxy} The proxy + * @static + * @inheritable */ getProxy: function() { return this.proxy; }, /** - * Static. Asynchronously loads a model instance by id. Sample usage: -

-    MyApp.User = Ext.define('User', {
-        extend: 'Ext.data.Model',
-        fields: [
-            {name: 'id', type: 'int'},
-            {name: 'name', type: 'string'}
-        ]
-    });
-
-    MyApp.User.load(10, {
-        scope: this,
-        failure: function(record, operation) {
-            //do something if the load failed
-        },
-        success: function(record, operation) {
-            //do something if the load succeeded
-        },
-        callback: function(record, operation) {
-            //do something whether the load succeeded or failed
-        }
-    });
-    
+ * Asynchronously loads a model instance by id. Sample usage: + * + * MyApp.User = Ext.define('User', { + * extend: 'Ext.data.Model', + * fields: [ + * {name: 'id', type: 'int'}, + * {name: 'name', type: 'string'} + * ] + * }); + * + * MyApp.User.load(10, { + * scope: this, + * failure: function(record, operation) { + * //do something if the load failed + * }, + * success: function(record, operation) { + * //do something if the load succeeded + * }, + * callback: function(record, operation) { + * //do something whether the load succeeded or failed + * } + * }); + * * @param {Number} id The id of the model to load - * @param {Object} config Optional config object containing success, failure and callback functions, plus optional scope - * @member Ext.data.Model - * @method load + * @param {Object} config (optional) config object containing success, failure and callback functions, plus + * optional scope * @static + * @inheritable */ load: function(id, config) { config = Ext.apply({}, config); @@ -466,17 +471,15 @@ Ext.define('Ext.data.Model', { COMMIT : 'commit', /** - * Generates a sequential id. This method is typically called when a record is {@link #create}d - * and {@link #Record no id has been specified}. The id will automatically be assigned - * to the record. The returned id takes the form: - * {PREFIX}-{AUTO_ID}.
- * @param {Ext.data.Model} rec The record being created. The record does not exist, it's a {@link #phantom}. - * @return {String} auto-generated string id, "ext-record-i++'; + * Generates a sequential id. This method is typically called when a record is {@link Ext#create + * create}d and {@link #constructor no id has been specified}. The id will automatically be assigned to the + * record. The returned id takes the form: {PREFIX}-{AUTO_ID}. + * + * - **PREFIX** : String - Ext.data.Model.PREFIX (defaults to 'ext-record') + * - **AUTO_ID** : String - Ext.data.Model.AUTO_ID (defaults to 1 initially) + * + * @param {Ext.data.Model} rec The record being created. The record does not exist, it's a {@link #phantom}. + * @return {String} auto-generated string id, `"ext-record-i++"`; * @static */ id: function(rec) { @@ -486,65 +489,172 @@ Ext.define('Ext.data.Model', { return id; } }, - + /** - * Internal flag used to track whether or not the model instance is currently being edited. Read-only - * @property editing - * @type Boolean + * @cfg {String/Object} idgen + * The id generator to use for this model. The default id generator does not generate + * values for the {@link #idProperty}. + * + * 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.SequentialIdGenerator'], + * idgen: 'sequential', + * ... + * }); + * + * The above would generate {@link Ext.data.SequentialIdGenerator sequential} id's such + * as 1, 2, 3 etc.. + * + * Another useful id generator is {@link Ext.data.UuidGenerator}: + * + * Ext.define('MyApp.data.MyModel', { + * extend: 'Ext.data.Model', + * requires: ['Ext.data.UuidGenerator'], + * idgen: 'uuid', + * ... + * }); + * + * An id generation can also be further configured: + * + * Ext.define('MyApp.data.MyModel', { + * extend: 'Ext.data.Model', + * idgen: { + * 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', + * idgen: { + * type: 'sequential', + * id: 'xy' + * } + * }); + * + * Ext.define('MyApp.data.MyModelY', { + * extend: 'Ext.data.Model', + * idgen: { + * type: 'sequential', + * id: 'xy' + * } + * }); + * + * For more complex, shared id generators, a custom generator is the best approach. + * See {@link Ext.data.IdGenerator} for details on creating custom id generators. + * + * @markdown + */ + idgen: { + isGenerator: true, + type: 'default', + + generate: function () { + return null; + }, + getRecId: function (rec) { + return rec.modelName + '-' + rec.internalId; + } + }, + + /** + * @property {Boolean} editing + * Internal flag used to track whether or not the model instance is currently being edited. Read-only. */ editing : false, /** - * Readonly flag - true if this Record has been modified. - * @type Boolean + * @property {Boolean} dirty + * True if this Record has been modified. Read-only. */ dirty : false, /** - * @cfg {String} persistanceProperty The property on this Persistable object that its data is saved to. - * Defaults to 'data' (e.g. all persistable data resides in this.data.) + * @cfg {String} persistenceProperty + * The property on this Persistable object that its data is saved to. Defaults to 'data' + * (e.g. all persistable data resides in this.data.) */ - persistanceProperty: 'data', + persistenceProperty: 'data', evented: false, isModel: true, /** - * true when the record does not yet exist in a server-side database (see - * {@link #setDirty}). Any record which has a real database pk set as its id property - * is NOT a phantom -- it's real. - * @property phantom - * @type {Boolean} + * @property {Boolean} phantom + * True when the record does not yet exist in a server-side database (see {@link #setDirty}). + * Any record which has a real database pk set as its id property is NOT a phantom -- it's real. */ phantom : false, /** - * @cfg {String} idProperty The name of the field treated as this Model's unique id (defaults to 'id'). + * @cfg {String} idProperty + * The name of the field treated as this Model's unique id. Defaults to 'id'. */ idProperty: 'id', /** - * The string type of the default Model Proxy. Defaults to 'ajax' - * @property defaultProxyType - * @type String + * @cfg {String} defaultProxyType + * The string type of the default Model Proxy. Defaults to 'ajax'. */ defaultProxyType: 'ajax', + // Fields config and property /** - * An array of the fields defined on this model - * @property fields - * @type {Array} + * @cfg {Object[]/String[]} fields + * The fields for this model. + */ + /** + * @property {Ext.util.MixedCollection} fields + * The fields defined on this model. */ - constructor: function(data, id) { + /** + * @cfg {Object[]} validations + * An array of {@link Ext.data.validations validations} for this model. + */ + + // Associations configs and properties + /** + * @cfg {Object[]} associations + * An array of {@link Ext.data.Association associations} for this model. + */ + /** + * @cfg {String/Object/String[]/Object[]} hasMany + * One or more {@link Ext.data.HasManyAssociation HasMany associations} for this model. + */ + /** + * @cfg {String/Object/String[]/Object[]} belongsTo + * One or more {@link Ext.data.BelongsToAssociation BelongsTo associations} for this model. + */ + /** + * @property {Ext.util.MixedCollection} associations + * {@link Ext.data.Association Associations} defined on this model. + */ + + /** + * @cfg {String/Object/Ext.data.proxy.Proxy} proxy + * The {@link Ext.data.proxy.Proxy proxy} to use for this model. + */ + + // raw not documented intentionally, meant to be used internally. + constructor: function(data, id, raw) { data = data || {}; - + var me = this, fields, length, field, name, i, + newId, isArray = Ext.isArray(data), newData = isArray ? {} : null; // to hold mapped array data if needed @@ -556,18 +666,30 @@ Ext.define('Ext.data.Model', { */ me.internalId = (id || id === 0) ? id : Ext.data.Model.id(me); + /** + * @property {Object} raw The raw data used to create this model if created via a reader. + */ + me.raw = raw; + Ext.applyIf(me, { - data: {} + data: {} }); - + /** - * Key: value pairs of all fields whose values have changed - * @property modified - * @type Object + * @property {Object} modified Key: value pairs of all fields whose values have changed */ me.modified = {}; - me[me.persistanceProperty] = {}; + // Deal with spelling error in previous releases + if (me.persistanceProperty) { + // + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.data.Model: persistanceProperty has been deprecated. Use persistenceProperty instead.'); + } + // + me.persistenceProperty = me.persistanceProperty; + } + me[me.persistenceProperty] = {}; me.mixins.observable.constructor.call(me); @@ -579,7 +701,7 @@ Ext.define('Ext.data.Model', { field = fields[i]; name = field.name; - if (isArray){ + if (isArray){ // Have to map array data so the values get assigned to the named fields // rather than getting set as the field names with undefined values. newData[name] = data[i]; @@ -590,52 +712,58 @@ Ext.define('Ext.data.Model', { } me.set(newData || data); - // clear any dirty/modified since we're initializing - me.dirty = false; - me.modified = {}; if (me.getId()) { me.phantom = false; + } else if (me.phantom) { + newId = me.idgen.generate(); + if (newId !== null) { + me.setId(newId); + } } + // clear any dirty/modified since we're initializing + me.dirty = false; + me.modified = {}; + if (typeof me.init == 'function') { me.init(); } - me.id = me.modelName + '-' + me.internalId; - - Ext.ModelManager.register(me); + me.id = me.idgen.getRecId(me); }, - + /** * Returns the value of the given field * @param {String} fieldName The field to fetch the value for - * @return {Mixed} The value + * @return {Object} The value */ get: function(field) { - return this[this.persistanceProperty][field]; + return this[this.persistenceProperty][field]; }, - + /** * Sets the given field to the given value, marks the instance as dirty - * @param {String|Object} fieldName The field to set, or an object containing key/value pairs - * @param {Mixed} value The value to set + * @param {String/Object} fieldName The field to set, or an object containing key/value pairs + * @param {Object} value The value to set */ set: function(fieldName, value) { var me = this, fields = me.fields, modified = me.modified, convertFields = [], - field, key, i, currentValue; + field, key, i, currentValue, notEditing, count, length; /* * If we're passed an object, iterate over that object. NOTE: we pull out fields with a convert function and * set those last so that all other possible data is set before the convert function is called */ if (arguments.length == 1 && Ext.isObject(fieldName)) { + notEditing = !me.editing; + count = 0; for (key in fieldName) { if (fieldName.hasOwnProperty(key)) { - + //here we check for the custom convert function. Note that if a field doesn't have a convert function, //we default it to its type's convert function, so we have to check that here. This feels rather dirty. field = fields.get(key); @@ -643,16 +771,30 @@ Ext.define('Ext.data.Model', { convertFields.push(key); continue; } - + + if (!count && notEditing) { + me.beginEdit(); + } + ++count; me.set(key, fieldName[key]); } } - for (i = 0; i < convertFields.length; i++) { - field = convertFields[i]; - me.set(field, fieldName[field]); + length = convertFields.length; + if (length) { + if (!count && notEditing) { + me.beginEdit(); + } + count += length; + for (i = 0; i < length; i++) { + field = convertFields[i]; + me.set(field, fieldName[field]); + } } + if (notEditing && count) { + me.endEdit(); + } } else { if (fields) { field = fields.get(fieldName); @@ -662,11 +804,28 @@ Ext.define('Ext.data.Model', { } } currentValue = me.get(fieldName); - me[me.persistanceProperty][fieldName] = value; - + me[me.persistenceProperty][fieldName] = value; + if (field && field.persist && !me.isEqual(currentValue, value)) { - me.dirty = true; - me.modified[fieldName] = currentValue; + if (me.isModified(fieldName)) { + if (me.isEqual(modified[fieldName], value)) { + // the original value in me.modified equals the new value, so the + // field is no longer modified + delete modified[fieldName]; + // we might have removed the last modified field, so check to see if + // there are any modified fields remaining and correct me.dirty: + me.dirty = false; + for (key in modified) { + if (modified.hasOwnProperty(key)){ + me.dirty = true; + break; + } + } + } + } else { + me.dirty = true; + modified[fieldName] = currentValue; + } } if (!me.editing) { @@ -674,7 +833,7 @@ Ext.define('Ext.data.Model', { } } }, - + /** * Checks if two values are equal, taking into account certain * special factors, for example dates. @@ -689,22 +848,21 @@ Ext.define('Ext.data.Model', { } return a === b; }, - + /** - * Begin an edit. While in edit mode, no events (e.g.. the update event) - * are relayed to the containing store. When an edit has begun, it must be followed - * by either {@link #endEdit} or {@link #cancelEdit}. + * Begins an edit. While in edit mode, no events (e.g.. the `update` event) are relayed to the containing store. + * When an edit has begun, it must be followed by either {@link #endEdit} or {@link #cancelEdit}. */ beginEdit : function(){ var me = this; if (!me.editing) { me.editing = true; me.dirtySave = me.dirty; - me.dataSave = Ext.apply({}, me[me.persistanceProperty]); + me.dataSave = Ext.apply({}, me[me.persistenceProperty]); me.modifiedSave = Ext.apply({}, me.modified); } }, - + /** * Cancels all changes made in the current edit operation. */ @@ -714,35 +872,61 @@ Ext.define('Ext.data.Model', { me.editing = false; // reset the modified state, nothing changed since the edit began me.modified = me.modifiedSave; - me[me.persistanceProperty] = me.dataSave; + me[me.persistenceProperty] = me.dataSave; me.dirty = me.dirtySave; delete me.modifiedSave; delete me.dataSave; delete me.dirtySave; } }, - + /** - * End an edit. If any data was modified, the containing store is notified - * (ie, the store's update event will fire). + * Ends an edit. If any data was modified, the containing store is notified (ie, the store's `update` event will + * fire). * @param {Boolean} silent True to not notify the store of the change */ endEdit : function(silent){ - var me = this; + var me = this, + didChange; + if (me.editing) { me.editing = false; + didChange = me.dirty || me.changedWhileEditing(); delete me.modifiedSave; delete me.dataSave; delete me.dirtySave; - if (silent !== true && me.dirty) { + if (silent !== true && didChange) { me.afterEdit(); } } }, + /** + * Checks if the underlying data has changed during an edit. This doesn't necessarily + * mean the record is dirty, however we still need to notify the store since it may need + * to update any views. + * @private + * @return {Boolean} True if the underlying data has changed during an edit. + */ + changedWhileEditing: function(){ + var me = this, + saved = me.dataSave, + data = me[me.persistenceProperty], + key; + + for (key in data) { + if (data.hasOwnProperty(key)) { + if (!me.isEqual(data[key], saved[key])) { + return true; + } + } + } + return false; + }, + /** * Gets a hash of only the fields that have been modified since this Model was created or commited. - * @return Object + * @return {Object} */ getChanges : function(){ var modified = this.modified, @@ -757,30 +941,27 @@ Ext.define('Ext.data.Model', { return changes; }, - + /** - * Returns true if the passed field name has been {@link #modified} - * since the load or last commit. + * Returns true if the passed field name has been `{@link #modified}` since the load or last commit. * @param {String} fieldName {@link Ext.data.Field#name} * @return {Boolean} */ isModified : function(fieldName) { return this.modified.hasOwnProperty(fieldName); }, - + /** - *

Marks this Record as {@link #dirty}. This method - * is used interally when adding {@link #phantom} records to a - * {@link Ext.data.Store#writer writer enabled store}.

- *

Marking a record {@link #dirty} causes the phantom to - * be returned by {@link Ext.data.Store#getModifiedRecords} where it will - * have a create action composed for it during {@link Ext.data.Store#save store save} - * operations.

+ * Marks this **Record** as `{@link #dirty}`. This method is used interally when adding `{@link #phantom}` records + * to a {@link Ext.data.proxy.Server#writer writer enabled store}. + * + * Marking a record `{@link #dirty}` causes the phantom to be returned by {@link Ext.data.Store#getUpdatedRecords} + * where it will have a create action composed for it during {@link Ext.data.Model#save model save} operations. */ setDirty : function() { var me = this, name; - + me.dirty = true; me.fields.each(function(field) { @@ -799,15 +980,17 @@ Ext.define('Ext.data.Model', { return this.setDirty.apply(this, arguments); }, // - + /** - * Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. - * Rejects all changes made to the model instance since either creation, or the last commit operation. - * Modified fields are reverted to their original values. - *

Developers should subscribe to the {@link Ext.data.Store#update} event - * to have their code notified of reject operations.

- * @param {Boolean} silent (optional) True to skip notification of the owning - * store of the change (defaults to false) + * Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. Rejects + * all changes made to the model instance since either creation, or the last commit operation. Modified fields are + * reverted to their original values. + * + * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of reject + * operations. + * + * @param {Boolean} silent (optional) True to skip notification of the owning store of the change. + * Defaults to false. */ reject : function(silent) { var me = this, @@ -817,7 +1000,7 @@ Ext.define('Ext.data.Model', { for (field in modified) { if (modified.hasOwnProperty(field)) { if (typeof modified[field] != "function") { - me[me.persistanceProperty][field] = modified[field]; + me[me.persistenceProperty][field] = modified[field]; } } } @@ -832,19 +1015,19 @@ Ext.define('Ext.data.Model', { }, /** - * Usually called by the {@link Ext.data.Store} which owns the model instance. - * Commits all changes made to the instance since either creation or the last commit operation. - *

Developers should subscribe to the {@link Ext.data.Store#update} event - * to have their code notified of commit operations.

- * @param {Boolean} silent (optional) True to skip notification of the owning - * store of the change (defaults to false) + * Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all changes made to the + * instance since either creation or the last commit operation. + * + * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of commit + * operations. + * + * @param {Boolean} silent (optional) True to skip notification of the owning store of the change. + * Defaults to false. */ commit : function(silent) { var me = this; - - me.dirty = false; - me.editing = false; + me.phantom = me.dirty = me.editing = false; me.modified = {}; if (silent !== true) { @@ -854,24 +1037,27 @@ Ext.define('Ext.data.Model', { /** * Creates a copy (clone) of this Model instance. - * @param {String} id (optional) A new id, defaults to the id - * of the instance being copied. See {@link #id}. - * To generate a phantom instance with a new id use:

-var rec = record.copy(); // clone the record
-Ext.data.Model.id(rec); // automatically generate a unique sequential id
-     * 
- * @return {Record} + * + * @param {String} [id] A new id, defaults to the id of the instance being copied. + * See `{@link Ext.data.Model#id id}`. To generate a phantom instance with a new id use: + * + * var rec = record.copy(); // clone the record + * Ext.data.Model.id(rec); // automatically generate a unique sequential id + * + * @return {Ext.data.Model} */ copy : function(newId) { var me = this; - - return new me.self(Ext.apply({}, me[me.persistanceProperty]), newId || me.internalId); + + return new me.self(Ext.apply({}, me[me.persistenceProperty]), newId || me.internalId); }, /** - * Sets the Proxy to use for this model. Accepts any options that can be accepted by {@link Ext#createByAlias Ext.createByAlias} + * 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 - * @static + * @return {Ext.data.proxy.Proxy} */ setProxy: function(proxy) { //make sure we have an Ext.data.proxy.Proxy object @@ -890,7 +1076,7 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id }, /** - * Returns the configured Proxy for this Model + * Returns the configured Proxy for this Model. * @return {Ext.data.proxy.Proxy} The proxy */ getProxy: function() { @@ -898,8 +1084,7 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id }, /** - * Validates the current data against all of its configured {@link #validations} and returns an - * {@link Ext.data.Errors Errors} object + * Validates the current data against all of its configured {@link #validations}. * @return {Ext.data.Errors} The errors object */ validate: function() { @@ -938,8 +1123,8 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id }, /** - * Saves the model instance using the configured proxy - * @param {Object} options Options to pass to the proxy + * Saves the model instance using the configured proxy. + * @param {Object} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}. * @return {Ext.data.Model} The Model instance */ save: function(options) { @@ -981,8 +1166,8 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id }, /** - * Destroys the model using the configured proxy - * @param {Object} options Options to pass to the proxy + * Destroys the model using the configured proxy. + * @param {Object} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}. * @return {Ext.data.Model} The Model instance */ destroy: function(options){ @@ -1014,7 +1199,7 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id }, /** - * Returns the unique ID allocated to this model instance as defined by {@link #idProperty} + * Returns the unique ID allocated to this model instance as defined by {@link #idProperty}. * @return {Number} The id */ getId: function() { @@ -1022,7 +1207,7 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id }, /** - * Sets the model instance's id field to the given id + * Sets the model instance's id field to the given id. * @param {Number} id The new id */ setId: function(id) { @@ -1030,22 +1215,22 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id }, /** - * Tells this model instance that it has been added to a store - * @param {Ext.data.Store} store The store that the model has been added to + * Tells this model instance that it has been added to a store. + * @param {Ext.data.Store} store The store to which this model has been added. */ join : function(store) { /** - * The {@link Ext.data.Store} to which this Record belongs. - * @property store - * @type {Ext.data.Store} + * @property {Ext.data.Store} store + * The {@link Ext.data.Store Store} to which this Record belongs. */ this.store = store; }, /** - * Tells this model instance that it has been removed from the store + * Tells this model instance that it has been removed from the store. + * @param {Ext.data.Store} store The store from which this model has been removed. */ - unjoin: function() { + unjoin: function(store) { delete this.store; }, @@ -1092,20 +1277,21 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id }, /** - * 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: [ - * ... - * ] - * } - * ] - * } + * Gets all of the data from this Models *loaded* associations. It does this recursively - for example if we have a + * User which hasMany Orders, and each Order hasMany OrderItems, it will return an object like this: + * + * { + * orders: [ + * { + * id: 123, + * status: 'shipped', + * orderItems: [ + * ... + * ] + * } + * ] + * } + * * @return {Object} The nested data set for the Model's loaded associations */ getAssociatedData: function(){ @@ -1117,7 +1303,7 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id * This complex-looking method takes a given Model instance and returns an object containing all data from * all of that Model's *loaded* associations. See (@link #getAssociatedData} * @param {Ext.data.Model} record The Model instance - * @param {Array} ids PRIVATE. The set of Model instance internalIds that have already been loaded + * @param {String[]} ids PRIVATE. The set of Model instance internalIds that have already been loaded * @param {String} associationType (optional) The name of the type of association to limit to. * @return {Object} The nested data set for the Model's loaded associations */ @@ -1181,3 +1367,4 @@ Ext.data.Model.id(rec); // automatically generate a unique sequential id return associationData; } }); +