3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
17 * @class Ext.data.reader.Reader
20 * <p>Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance or a {@link Ext.data.Store Store}
21 * - usually in response to an AJAX request. This is normally handled transparently by passing some configuration to either the
22 * {@link Ext.data.Model Model} or the {@link Ext.data.Store Store} in question - see their documentation for further details.</p>
24 * <p><u>Loading Nested Data</u></p>
26 * <p>Readers have the ability to automatically load deeply-nested data objects based on the {@link Ext.data.Association associations}
27 * configured on each Model. Below is an example demonstrating the flexibility of these associations in a fictional CRM system which
28 * manages a User, their Orders, OrderItems and Products. First we'll define the models:
32 extend: 'Ext.data.Model',
37 hasMany: {model: 'Order', name: 'orders'},
50 extend: 'Ext.data.Model',
55 hasMany : {model: 'OrderItem', name: 'orderItems', associationKey: 'order_items'},
59 Ext.define("OrderItem", {
60 extend: 'Ext.data.Model',
62 'id', 'price', 'quantity', 'order_id', 'product_id'
65 belongsTo: ['Order', {model: 'Product', associationKey: 'product'}]
68 Ext.define("Product", {
69 extend: 'Ext.data.Model',
78 * <p>This may be a lot to take in - basically a User has many Orders, each of which is composed of several OrderItems. Finally,
79 * each OrderItem has a single Product. This allows us to consume data like this:</p>
118 * <p>The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's sake), all of the Orders
119 * for each User (again just 1 in this case), all of the OrderItems for each Order (2 order items in this case), and finally
120 * the Product associated with each OrderItem. Now we can read the data and use it as follows:
123 var store = new Ext.data.Store({
128 callback: function() {
129 //the user that was loaded
130 var user = store.first();
132 console.log("Orders for " + user.get('name') + ":")
134 //iterate over the Orders for each User
135 user.orders().each(function(order) {
136 console.log("Order ID: " + order.getId() + ", which contains items:");
138 //iterate over the OrderItems for each Order
139 order.orderItems().each(function(orderItem) {
140 //we know that the Product data is already loaded, so we can use the synchronous getProduct
141 //usually, we would use the asynchronous version (see {@link Ext.data.BelongsToAssociation})
142 var product = orderItem.getProduct();
144 console.log(orderItem.get('quantity') + ' orders of ' + product.get('name'));
151 * <p>Running the code above results in the following:</p>
155 Order ID: 50, which contains items:
156 2 orders of MacBook Pro
161 Ext.define('Ext.data.reader.Reader', {
162 requires: ['Ext.data.ResultSet'],
163 alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'],
166 * @cfg {String} idProperty Name of the property within a row object
167 * that contains a record identifier value. Defaults to <tt>The id of the model</tt>.
168 * If an idProperty is explicitly specified it will override that of the one specified
173 * @cfg {String} totalProperty Name of the property from which to
174 * retrieve the total number of records in the dataset. This is only needed
175 * if the whole dataset is not passed in one go, but is being paged from
176 * the remote server. Defaults to <tt>total</tt>.
178 totalProperty: 'total',
181 * @cfg {String} successProperty Name of the property from which to
182 * retrieve the success attribute. Defaults to <tt>success</tt>. See
183 * {@link Ext.data.proxy.Proxy}.{@link Ext.data.proxy.Proxy#exception exception}
184 * for additional information.
186 successProperty: 'success',
189 * @cfg {String} root <b>Required</b>. The name of the property
190 * which contains the Array of row objects. Defaults to <tt>undefined</tt>.
191 * An exception will be thrown if the root property is undefined. The data
192 * packet value for this property should be an empty array to clear the data
198 * @cfg {String} messageProperty The name of the property which contains a response message.
199 * This property is optional.
203 * @cfg {Boolean} implicitIncludes True to automatically parse models nested within other models in a response
204 * object. See the Ext.data.reader.Reader intro docs for full explanation. Defaults to true.
206 implicitIncludes: true,
211 * Creates new Reader.
212 * @param {Object} config (optional) Config object.
214 constructor: function(config) {
217 Ext.apply(me, config || {});
219 me.model = Ext.ModelManager.getModel(config.model);
221 me.buildExtractors();
226 * Sets a new model for the reader.
228 * @param {Object} model The model to set.
229 * @param {Boolean} setOnProxy True to also set on the Proxy, if one is configured
231 setModel: function(model, setOnProxy) {
234 me.model = Ext.ModelManager.getModel(model);
235 me.buildExtractors(true);
237 if (setOnProxy && me.proxy) {
238 me.proxy.setModel(me.model, true);
243 * Reads the given response object. This method normalizes the different types of response object that may be passed
244 * to it, before handing off the reading of records to the {@link #readRecords} function.
245 * @param {Object} response The response object. This may be either an XMLHttpRequest object or a plain JS object
246 * @return {Ext.data.ResultSet} The parsed ResultSet object
248 read: function(response) {
251 if (response && response.responseText) {
252 data = this.getResponseData(response);
256 return this.readRecords(data);
258 return this.nullResultSet;
263 * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to call
264 * this function before running its own logic and returning the Ext.data.ResultSet instance. For most
265 * Readers additional processing should not be needed.
266 * @param {Mixed} data The raw data object
267 * @return {Ext.data.ResultSet} A ResultSet object
269 readRecords: function(data) {
273 * We check here whether the number of fields has changed since the last read.
274 * This works around an issue when a Model is used for both a Tree and another
275 * source, because the tree decorates the model with extra fields and it causes
276 * issues because the readers aren't notified.
278 if (me.fieldCount !== me.getFields().length) {
279 me.buildExtractors(true);
283 * The raw data object that was last passed to readRecords. Stored for further processing if needed
289 data = me.getData(data);
291 // If we pass an array as the data, we dont use getRoot on the data.
292 // Instead the root equals to the data.
293 var root = Ext.isArray(data) ? data : me.getRoot(data),
296 total, value, records, message;
302 if (me.totalProperty) {
303 value = parseInt(me.getTotal(data), 10);
309 if (me.successProperty) {
310 value = me.getSuccess(data);
311 if (value === false || value === 'false') {
316 if (me.messageProperty) {
317 message = me.getMessage(data);
321 records = me.extractData(root);
322 recordCount = records.length;
328 return Ext.create('Ext.data.ResultSet', {
329 total : total || recordCount,
338 * Returns extracted, type-cast rows of data. Iterates to call #extractValues for each row
339 * @param {Object[]/Object} data-root from server response
342 extractData : function(root) {
348 length = root.length,
349 idProp = me.getIdProperty(),
352 if (!root.length && Ext.isObject(root)) {
357 for (; i < length; i++) {
359 values = me.extractValues(node);
363 record = new Model(values, id, node);
364 records.push(record);
366 if (me.implicitIncludes) {
367 me.readAssociated(record, node);
376 * Loads a record's associations from the data object. This prepopulates hasMany and belongsTo associations
377 * on the record provided.
378 * @param {Ext.data.Model} record The record to load associations for
379 * @param {Mixed} data The data object
380 * @return {String} Return value description
382 readAssociated: function(record, data) {
383 var associations = record.associations.items,
385 length = associations.length,
386 association, associationData, proxy, reader;
388 for (; i < length; i++) {
389 association = associations[i];
390 associationData = this.getAssociatedDataRoot(data, association.associationKey || association.name);
392 if (associationData) {
393 reader = association.getReader();
395 proxy = association.associatedModel.proxy;
396 // if the associated model has a Reader already, use that, otherwise attempt to create a sensible one
398 reader = proxy.getReader();
400 reader = new this.constructor({
401 model: association.associatedName
405 association.read(record, reader, associationData);
412 * Used internally by {@link #readAssociated}. Given a data object (which could be json, xml etc) for a specific
413 * record, this should return the relevant part of that data for the given association name. This is only really
414 * needed to support the XML Reader, which has to do a query to get the associated data object
415 * @param {Mixed} data The raw data object
416 * @param {String} associationName The name of the association to get data for (uses associationKey if present)
417 * @return {Mixed} The root
419 getAssociatedDataRoot: function(data, associationName) {
420 return data[associationName];
423 getFields: function() {
424 return this.model.prototype.fields.items;
429 * Given an object representing a single model instance's data, iterates over the model's fields and
430 * builds an object with the value for each field.
431 * @param {Object} data The data object to convert
432 * @return {Object} Data object suitable for use with a model constructor
434 extractValues: function(data) {
435 var fields = this.getFields(),
437 length = fields.length,
441 for (; i < length; i++) {
443 value = this.extractorFunctions[i](data);
445 output[field.name] = value;
453 * By default this function just returns what is passed to it. It can be overridden in a subclass
454 * to return something else. See XmlReader for an example.
455 * @param {Object} data The data object
456 * @return {Object} The normalized data object
458 getData: function(data) {
464 * This will usually need to be implemented in a subclass. Given a generic data object (the type depends on the type
465 * of data we are reading), this function should return the object as configured by the Reader's 'root' meta data config.
466 * See XmlReader's getRoot implementation for an example. By default the same data object will simply be returned.
467 * @param {Mixed} data The data object
468 * @return {Mixed} The same data object
470 getRoot: function(data) {
475 * Takes a raw response object (as passed to this.read) and returns the useful data segment of it. This must be implemented by each subclass
476 * @param {Object} response The responce object
477 * @return {Object} The useful data from the response
479 getResponseData: function(response) {
481 Ext.Error.raise("getResponseData must be implemented in the Ext.data.reader.Reader subclass");
487 * Reconfigures the meta data tied to this Reader
489 onMetaChange : function(meta) {
490 var fields = meta.fields,
493 Ext.apply(this, meta);
496 newModel = Ext.define("Ext.data.reader.Json-Model" + Ext.id(), {
497 extend: 'Ext.data.Model',
500 this.setModel(newModel, true);
502 this.buildExtractors(true);
507 * Get the idProperty to use for extracting data
509 * @return {String} The id property
511 getIdProperty: function(){
512 var prop = this.idProperty;
513 if (Ext.isEmpty(prop)) {
514 prop = this.model.prototype.idProperty;
521 * This builds optimized functions for retrieving record data and meta data from an object.
522 * Subclasses may need to implement their own getRoot function.
523 * @param {Boolean} force True to automatically remove existing extractor functions first (defaults to false)
525 buildExtractors: function(force) {
527 idProp = me.getIdProperty(),
528 totalProp = me.totalProperty,
529 successProp = me.successProperty,
530 messageProp = me.messageProperty,
533 if (force === true) {
534 delete me.extractorFunctions;
537 if (me.extractorFunctions) {
541 //build the extractors for all the meta data
543 me.getTotal = me.createAccessor(totalProp);
547 me.getSuccess = me.createAccessor(successProp);
551 me.getMessage = me.createAccessor(messageProp);
555 accessor = me.createAccessor(idProp);
557 me.getId = function(record) {
558 var id = accessor.call(me, record);
559 return (id === undefined || id === '') ? null : id;
562 me.getId = function() {
566 me.buildFieldExtractors();
572 buildFieldExtractors: function() {
573 //now build the extractors for all the fields
575 fields = me.getFields(),
578 extractorFunctions = [],
581 for (; i < ln; i++) {
583 map = (field.mapping !== undefined && field.mapping !== null) ? field.mapping : field.name;
585 extractorFunctions.push(me.createAccessor(map));
589 me.extractorFunctions = extractorFunctions;
593 // Private. Empty ResultSet to return when response is falsy (null|undefined|empty string)
594 nullResultSet: Ext.create('Ext.data.ResultSet', {