/** * @author Ed Spencer * @class Ext.data.Reader * @extends Object * *

Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance or a {@link Ext.data.Store Store} * - usually in response to an AJAX request. This is normally handled transparently by passing some configuration to either the * {@link Ext.data.Model Model} or the {@link Ext.data.Store Store} in question - see their documentation for further details.

* *

Loading Nested Data

* *

Readers have the ability to automatically load deeply-nested data objects based on the {@link Ext.data.Association associations} * configured on each Model. Below is an example demonstrating the flexibility of these associations in a fictional CRM system which * manages a User, their Orders, OrderItems and Products. First we'll define the models: *


Ext.regModel("User", {
    fields: [
        'id', 'name'
    ],

    hasMany: {model: 'Order', name: 'orders'},

    proxy: {
        type: 'rest',
        url : 'users.json',
        reader: {
            type: 'json',
            root: 'users'
        }
    }
});

Ext.regModel("Order", {
    fields: [
        'id', 'total'
    ],

    hasMany  : {model: 'OrderItem', name: 'orderItems', associationKey: 'order_items'},
    belongsTo: 'User'
});

Ext.regModel("OrderItem", {
    fields: [
        'id', 'price', 'quantity', 'order_id', 'product_id'
    ],

    belongsTo: ['Order', {model: 'Product', associationKey: 'product'}]
});

Ext.regModel("Product", {
    fields: [
        'id', 'name'
    ],

    hasMany: 'OrderItem'
});
* *

This may be a lot to take in - basically a User has many Orders, each of which is composed of several OrderItems. Finally, * each OrderItem has a single Product. This allows us to consume data like this:

*

{
    "users": [
        {
            "id": 123,
            "name": "Ed",
            "orders": [
                {
                    "id": 50,
                    "total": 100,
                    "order_items": [
                        {
                            "id"      : 20,
                            "price"   : 40,
                            "quantity": 2,
                            "product" : {
                                "id": 1000,
                                "name": "MacBook Pro"
                            }
                        },
                        {
                            "id"      : 21,
                            "price"   : 20,
                            "quantity": 3,
                            "product" : {
                                "id": 1001,
                                "name": "iPhone"
                            }
                        }
                    ]
                }
            ]
        }
    ]
}
* *

The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's sake), all of the Orders * for each User (again just 1 in this case), all of the OrderItems for each Order (2 order items in this case), and finally * the Product associated with each OrderItem. Now we can read the data and use it as follows: *


var store = new Ext.data.Store({
    model: "User"
});

store.load({
    callback: function() {
        //the user that was loaded
        var user = store.first();

        console.log("Orders for " + user.get('name') + ":")

        //iterate over the Orders for each User
        user.orders().each(function(order) {
            console.log("Order ID: " + order.getId() + ", which contains items:");

            //iterate over the OrderItems for each Order
            order.orderItems().each(function(orderItem) {
                //we know that the Product data is already loaded, so we can use the synchronous getProduct
                //usually, we would use the asynchronous version (see {@link Ext.data.BelongsToAssociation})
                var product = orderItem.getProduct();

                console.log(orderItem.get('quantity') + ' orders of ' + product.get('name'));
            });
        });
    }
});
* *

Running the code above results in the following:

*

Orders for Ed:
Order ID: 50, which contains items:
2 orders of MacBook Pro
3 orders of iPhone
* * @constructor * @param {Object} config Optional config object */ Ext.define('Ext.data.Reader', { requires: ['Ext.data.ResultSet'],
/** * @cfg {String} idProperty Name of the property within a row object * that contains a record identifier value. Defaults to id */ idProperty: 'id',
/** * @cfg {String} totalProperty Name of the property from which to * retrieve the total number of records in the dataset. This is only needed * if the whole dataset is not passed in one go, but is being paged from * the remote server. Defaults to total. */ totalProperty: 'total',
/** * @cfg {String} successProperty Name of the property from which to * retrieve the success attribute. Defaults to success. See * {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} * for additional information. */ successProperty: 'success',
/** * @cfg {String} root Required. The name of the property * which contains the Array of row objects. Defaults to undefined. * An exception will be thrown if the root property is undefined. The data * packet value for this property should be an empty array to clear the data * or show no data. */ root: '',
/** * @cfg {String} messageProperty The name of the property which contains a response message. * This property is optional. */
/** * @cfg {Boolean} implicitIncludes True to automatically parse models nested within other models in a response * object. See the Ext.data.Reader intro docs for full explanation. Defaults to true. */ implicitIncludes: true, constructor: function(config) { Ext.apply(this, config || {}); this.model = Ext.ModelMgr.getModel(config.model); if (this.model) { this.buildExtractors(); } }, /** * Sets a new model for the reader. * @private * @param {Object} model The model to set. * @param {Boolean} setOnProxy True to also set on the Proxy, if one is configured */ setModel: function(model, setOnProxy) { this.model = Ext.ModelMgr.getModel(model); this.buildExtractors(true); if (setOnProxy && this.proxy) { this.proxy.setModel(this.model, true); } },
/** * Reads the given response object. This method normalizes the different types of response object that may be passed * to it, before handing off the reading of records to the {@link readRecords} function. * @param {Object} response The response object. This may be either an XMLHttpRequest object or a plain JS object * @return {Ext.data.ResultSet} The parsed ResultSet object */ read: function(response) { var data = response; if (response && response.responseText) { data = this.getResponseData(response); } if (data) { return this.readRecords(data); } else { return this.nullResultSet; } },
/** * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to call * this function before running its own logic and returning the Ext.data.ResultSet instance. For most * Readers additional processing should not be needed. * @param {Mixed} data The raw data object * @return {Ext.data.ResultSet} A ResultSet object */ readRecords: function(data) {
/** * The raw data object that was last passed to readRecords. Stored for further processing if needed * @property rawData * @type Mixed */ this.rawData = data; data = this.getData(data); var root = this.getRoot(data), total = root.length, success = true, value, records, message, recordCount; if (this.totalProperty) { value = parseInt(this.getTotal(data), 10); if (!isNaN(value)) { total = value; } } if (this.successProperty) { value = this.getSuccess(data); if (value === false || value === 'false') { success = false; } } if (this.messageProperty) { message = this.getMessage(data); } records = this.extractData(root, true); recordCount = records.length; return Ext.create('Ext.data.ResultSet', { total : total || recordCount, count : recordCount, records: records, success: success, message: message }); }, /** * Returns extracted, type-cast rows of data. Iterates to call #extractValues for each row * @param {Object[]/Object} data-root from server response * @param {Boolean} returnRecords [false] Set true to return instances of Ext.data.Record * @private */ extractData : function(root, returnRecords) { var values = [], records = [], Model = this.model, length = root.length, idProp = this.idProperty, node, id, record, i; for (i = 0; i < length; i++) { node = root[i]; values = this.extractValues(node); id = this.getId(node); if (returnRecords === true) { record = new Model(values, id); record.raw = node; records.push(record); if (this.implicitIncludes) { this.readAssociated(record, node); } } else { values[idProp] = id; records.push(values); } } return records; }, /** * @private * Loads a record's associations from the data object. This prepopulates hasMany and belongsTo associations * on the record provided. * @param {Ext.data.Model} record The record to load associations for * @param {Mixed} data The data object * @return {String} Return value description */ readAssociated: function(record, data) { var associations = record.associations.items, length = associations.length, association, associationName, associatedModel, associationData, inverseAssociation, proxy, reader, store, i; for (i = 0; i < length; i++) { association = associations[i]; associationName = association.name; associationData = this.getAssociatedDataRoot(data, association.associationKey || associationName); associatedModel = association.associatedModel; if (associationData) { proxy = associatedModel.proxy; // if the associated model has a Reader already, use that, otherwise attempt to create a sensible one if (proxy) { reader = proxy.getReader(); } else { reader = new this.constructor({ model: association.associatedName }); } if (association.type == 'hasMany') { store = record[associationName](); store.add.apply(store, reader.read(associationData).records); //now that we've added the related records to the hasMany association, set the inverse belongsTo //association on each of them if it exists inverseAssociation = associatedModel.prototype.associations.findBy(function(assoc) { return assoc.type == 'belongsTo' && assoc.associatedName == record.constructor.modelName; }); //if the inverse association was found, set it now on each record we've just created if (inverseAssociation) { store.data.each(function(associatedRecord) { associatedRecord[inverseAssociation.instanceName] = record; }); } } else if (association.type == 'belongsTo') { record[association.instanceName] = reader.read([associationData]).records[0]; } } } }, /** * @private * Used internally by {@link #readAssociated}. Given a data object (which could be json, xml etc) for a specific * record, this should return the relevant part of that data for the given association name. This is only really * needed to support the XML Reader, which has to do a query to get the associated data object * @param {Mixed} data The raw data object * @param {String} associationName The name of the association to get data for (uses associationKey if present) * @return {Mixed} The root */ getAssociatedDataRoot: function(data, associationName) { return data[associationName]; }, /** * @private * Given an object representing a single model instance's data, iterates over the model's fields and * builds an object with the value for each field. * @param {Object} data The data object to convert * @return {Object} Data object suitable for use with a model constructor */ extractValues: function(data) { var fields = this.model.prototype.fields.items, length = fields.length, output = {}, field, value, i; for (i = 0; i < length; i++) { field = fields[i]; value = this.extractorFunctions[i](data) || field.defaultValue; output[field.name] = value; } return output; }, /** * @private * By default this function just returns what is passed to it. It can be overridden in a subclass * to return something else. See XmlReader for an example. * @param {Object} data The data object * @return {Object} The normalized data object */ getData: function(data) { return data; }, /** * @private * This will usually need to be implemented in a subclass. Given a generic data object (the type depends on the type * of data we are reading), this function should return the object as configured by the Reader's 'root' meta data config. * See XmlReader's getRoot implementation for an example. By default the same data object will simply be returned. * @param {Mixed} data The data object * @return {Mixed} The same data object */ getRoot: function(data) { return data; },
/** * 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 * @param {Object} response The responce object * @return {Object} The useful data from the response */ getResponseData: function(response) { throw new Error("getResponseData must be implemented in the Ext.data.Reader subclass"); }, /** * @private * Reconfigures the meta data tied to this Reader */ onMetaChange : function(meta) { var fields = meta.fields, newModel; Ext.apply(this, meta); if (fields) { newModel = Ext.regModel("Ext.data.JsonReader-Model" + Ext.id(), {fields: fields}); this.setModel(newModel, true); } else { this.buildExtractors(true); } }, /** * @private * This builds optimized functions for retrieving record data and meta data from an object. * Subclasses may need to implement their own getRoot function. * @param {Boolean} force True to automatically remove existing extractor functions first (defaults to false) */ buildExtractors: function(force) { var me = this, idProp = this.id || this.idProperty, totalProp = this.totalProperty, successProp = this.successProperty, messageProp = this.messageProperty, acessor; if (force === true) { delete me.extractorFunctions; } if (me.extractorFunctions) { return; } //build the extractors for all the meta data if (totalProp) { me.getTotal = me.createAccessor(totalProp); } if (successProp) { me.getSuccess = me.createAccessor(successProp); } if (messageProp) { me.getMessage = me.createAccessor(messageProp); } if (idProp) { accessor = me.createAccessor(idProp); me.getId = function(record) { var id = accessor.call(me, record); return (id === undefined || id === '') ? null : id; }; } else { me.getId = function() { return null; }; } me.buildFieldExtractors(); }, /** * @private */ buildFieldExtractors: function() { //now build the extractors for all the fields var fields = this.model.prototype.fields.items, ln = fields.length, i = 0, extractorFunctions = [], field, map; for (; i < ln; i++) { field = fields[i]; map = (field.mapping !== undefined && field.mapping !== null) ? field.mapping : field.name; extractorFunctions.push(this.createAccessor(map)); } this.extractorFunctions = extractorFunctions; } }, function() { Ext.apply(this, { // Private. Empty ResultSet to return when response is falsy (null|undefined|empty string) nullResultSet: Ext.create('Ext.data.ResultSet', { total : 0, count : 0, records: [], success: true }) }); });