Skip to main content

ExtJS4 How to read and write nested data?

How to read nested JSON data

Sample JSON like this:
{
    id: 1234,
    name: 'Aaron Smith',
    address: {
        address1: '1925 Isaac Newton Sq',
        address2: 'Suite 300',
        city: 'Reston',
        state: 'VA',
        zip: 20190
    }
}
Your corresponding Model definition would look like so:
Ext.define('User', {
    extend: 'Ext.data.Model',
    fields: [
        'id', 
        'name',
        { name: 'address1', mapping: 'address.address1' },
        { name: 'address2', mapping: 'address.address2' },
        { name: 'city', mapping: 'address.city' },
        { name: 'state', mapping: 'address.state' },
        { name: 'zip', mapping: 'address.zip' }
    ]
});
But what if address isn’t provided on all read requests? Ext JS’s JsonReader chokes. Here’s a JsonReader extension that will traverse mappings safely:
Ext.define('MC.data.JsonReader', {
    extend: 'Ext.data.reader.Json',
    alias: 'reader.json',

    safeMappings: false,

    createFieldAccessExpression: (function() {
        var re = /[\[\.]/;

        return function(field, fieldVarName, dataName) {
            var hasMap = (field.mapping !== null),
                map    = hasMap ? field.mapping : field.name,
                result,
                operatorSearch,
                mapFields, mapResult, i, len, value;

            if (typeof map === 'function') {
                result = fieldVarName + '.mapping(' + dataName + ', this)';
            } else if (this.useSimpleAccessors === true || ((operatorSearch = String(map).search(re)) < 0)) {
                if (!hasMap || isNaN(map)) {
                    // If we don't provide a mapping, we may have a field name that is numeric
                    map = '"' + map + '"';
                }
                result = dataName + "[" + map + "]";
            } else if (this.safeMappings && operatorSearch > 0) {
                mapFields = map.split('.');
                mapResult = [];
                for (i=0, len=mapFields.length-1; i 0 ? '.' : '') + map;
            }
            return result;
        };
    }())

});
To minimize performance impact, safe mapping traversal is not enabled by default. For any Models that require this, simply configure the corresponding reader as such:
reader: {
    type: 'json',
    safeMappings: true
}

Writing Structured Data

Let’s say you need to write back data using the same format in which it is provided in a read operation. As you may know, the default JsonWriter does not use mappings to structure the data for writes and instead sends only key-value pairs. While it would be tempting to write a simple JsonWriter extension to achieve this. I believe this approach is shortsighted. It makes more sense to place this functionality on the Model prototype so that any obscure boundary cases can be handled with custom code that is easily isolated in the particular Model(s).
In order to achieve this, we need two extensions. First, we’ll extend Ext.data.Model to take an instance’s values and construct the payload:
Ext.define('MC.data.Model', {
    extend: 'Ext.data.Model',

    writeStructuredData: true,

    getWriteData: function() {
        var isPhantom = record.phantom === true,
            writeAllFields = this.writeAllFields,
            fields = this.fields,
            fieldItems = fields.items,
            data = {},
            changes = this.getChanges(),
            field,
            key,
            f, fLen;

        for (f=0, fLen=fieldItems.length; f<fLen; f++) {
            field = fieldItems[f];

            if (field.persist && (writeAllFields || isPhantom)) {
                this.setFieldWriteData(data, this, field, this.get(field.name);
            }
        }

        for (key in changes) {
            if (changes.hasOwnProperty(key)) {
                field = fields.get(key);
                if (field.persist) {
                    this.setFieldWriteData(data, this, field, changes[key]);
                }
            }
        }

        return data;
    },

    setFieldWriteData: function(data, record, field, value) {
        var name = field[this.nameProperty] || field.name,
            path, i, len, curr;

        if (field.name === record.idProperty && record.phantom) {
            return;
        }

        if (field.mapping) {
            if (field.mapping.indexOf('.')) {
                path = field.mapping.split('.');
                curr = data;
                for (i=0, len=path.length-1; i<len; i++) {
                    if (!curr[path[i]]) {
                        curr[path[i]] = {};
                    }
                    curr = curr[path[i]];
                }
                curr[path[i]] = value;
            }
        } else {
            data[name] = value;
        }
    }
});
Next, we’ll extend Ext.data.writer.Json to use Model’s getWriteData() when writeStructuredData is true. Note that this will not break down even if MC.data.Model is not used.
Ext.define('MC.data.JsonWriter', {
    extend: 'Ext.data.writer.Json',
    alias: 'writer.json',

    getRecordData: function(record, operation) {
        if (record.writeStructuredData) {
            return record.getWriteData();
        } else {
            return this.callParent(arguments);
        }
    }
});

Writing Associated Data

One oddity I found is that there was no way to write up associated data with the owning Model. This can easily be incorporated into the Model extension I provided above. We just need to restructure it so the getWriteData() method looks at the current record and any associated records. This is what it looks like.
Ext.define('MC.data.Model', {
    extend: 'Ext.data.Model',

    writeStructuredData: true,

    getWriteData: function() {
        var data = this.getRecordWriteData(this),
            associations = this.associations.items,
            association, type, name, associatedStore, 
            associatedRecords, associatedRecord,
            a, aLen, r, rLen;

        for (a=0, aLen=associations.length; a<aLen; a++) {

            association = associations[a];
            type = association.type;
            name = association.name;

            if (type == 'hasMany') {

                associatedStore = this[association.storeName];
                // Initialize the array for this association
                data[name] = [];

                // If the association's loaded, process its records
                if (associatedStore && associatedStore.getCount() > 0) {
                    associatedRecords = associatedStore.data.items;

                    // Append data for each record
                    for (r=0, rLen=associatedRecords.length; r<rLen; r++) {
                        data[name][r] = this.getRecordWriteData(associatedRecords[r]);
                    }
                }

            } else if (type == 'hasOne') {
                associatedRecord = this[association.instanceName];
                // If the record exists, append its data
                if (associatedRecord !== undefined) {
                    data[name] = this.getRecordWriteData(associatedRecord);
                }
            }

        }

        return data;
    },

    getRecordWriteData: function(record) {
        var isPhantom = record.phantom === true,
            writeAllFields = record.writeAllFields,
            fields = record.fields,
            fieldItems = fields.items,
            data = {},
            changes = record.getChanges(),
            field,
            key,
            f, fLen;

        for (f=0, fLen=fieldItems.length; f<fLen; f++) {
            field = fieldItems[f];

            if (field.forcePersist || (field.persist && (writeAllFields || isPhantom))) {
                this.setFieldWriteData(data, record, field, record.get(field.name);
            }
        }

        for (key in changes) {
            if (changes.hasOwnProperty(key)) {
                field = fields.get(key);
                if (field.persist) {
                    this.setFieldWriteData(data, record, field, changes[key]);
                }
            }
        }

        return data;
    },

    setFieldWriteData: function(data, record, field, value) {
        var name = field[this.nameProperty] || field.name,
            path, i, len, curr;

        if (field.name === record.idProperty && record.phantom) {
            return;
        }

        if (field.mapping) {
            if (field.mapping.indexOf('.')) {
                path = field.mapping.split('.');
                curr = data;
                for (i=0, len=path.length-1; i<len; i++) {
                    if (!curr[path[i]]) {
                        curr[path[i]] = {};
                    }
                    curr = curr[path[i]];
                }
                curr[path[i]] = value;
            }
        } else {
            data[name] = value;
        }
    }

});
One thing I added here was to allow you to specify a forcePersist property on any Field to make sure it is persisted whether it has changed or not. It is also worth noting that if you need to write associated Models with a different key than they are read you simply can use associationKey

// Bind store to grid using afterrender event Example
afterrender: function(panel){
var sampleStore = Ext.create('myApp.store.myStore');
panel.down('pagingtoolbar').bindStore(sampleStore);
panel.down('#refresh').hide();
panel.reconfigure(sampleStore);
}

Comments

Post a Comment

Popular posts from this blog

ExtJS - Grid panel features

What can we do with ExtJS GridPanel? I have to develop a lot of applications in my web app and I see that grid component of ExtJS may fit in. However, I am not aware of what all things I can do with the - off the shelf available framework pieces - available plug-ins in the marketplace and - custom development through my own developers This is a typical question that we hear from the business users who wants to design an application by keeping the framework’s capability in perspective. In this article I have tried to put the list of stuff you can do with grid and hopefully that shall enable you to take advantage of the beauty of ExtJS. Pre-requisites This article assumes that you are familiar with basics of ExtJS What are the available options? In this section I will be taking you through some of the commonly seen usage of ExtJS grid panel. While covering all the capabilities of grid may not be possible, I am sure it will be helpful for the business users who want to...

ExtJS 4 with Spring MVC Example

Introduction When developing a brand new application for the company I work for, one of the first thing I implement is "authentication". Not only is this process generally a prerequisite to access many other functions but it is also very simple and covers every layer of an application, from GUI to the database. So instead of another minimalistic "Hello World" tutorial, here is a "Login/Password" example, using 2 famous technologies : Spring MVC and ExtJS 4.   Prerequisites Because this tutorial is quite long, I won't go into the details of each technology I used but I will mainly focus on how ExtJS and Spring can be nicely used together. So here are the things you should know before continuing : Spring framework (good knowledge) Spring MVC (basic knowledge. See  Spring MVC Official Documentation  for details) ExtJS "application architecture" (see  Sencha Docs  for details) Eclipse IDE + Tomcat 6 (or any other web container) You a...

Ext4 Apply Store Filtering

In extjs4.1: There are many way for store filtering . Some of code i give here Filtering by single field: store . filter ( 'eyeColor' , 'Brown' );   Alternatively, if filters are configured with an  id , then existing filters store may be  replaced by new filters having the same  id . Filtering by single field: store . filter ( "email" , /\.com$/ );   Using multiple filters: store . filter ([ { property : "email" , value : /\.com$/ }, { filterFn : function ( item ) { return item . get ( "age" ) > 10 ; }} ]);   Using  Ext.util.Filter  instances instead of config objects (note that we need to specify the root config option in this case): store . filter ([ Ext . create ( ' Ext.util.Filter ' , {   property : "email" , value : /\.com$/ , root : 'data' }),   Ext . create ( ' Ext.util.Filter ' , {   filterFn : function ( item ) {   return item . get ( ...