1
votes

I have an ExtJS grid (Version 4.2.1). It is buffered and infinite scrolling. It is populated by a store with a REST proxy.

The request is made to:

/endpoints.json?_dc=1374783152564&page=1&start=0&limit=30&sort=checkedAt&dir=desc&query=codian

The json response is (truncated to 1 result):

{"links":[],
 "count":30,
 "totalCount":30,
 "endpointList":[{
    "links":[{"rel":"self","href":"http://localhost:8080/endpointmanager/endpoints/3"},
             {"rel":"currentStatus","href":"http://localhost:8080/endpointmanager/endpoints/3/status"},
             {"rel":"statusLog","href":"http://localhost:8080/endpointmanager/endpoints/3/statuslog"}],
    "name":"Codian MCU",     
    "managed":false,
    "updateInterval":3,
    "checkedAt":null,
    "timeout":60,
    "deviceType":"Codian MCU",
    "currentStatus":{"links":[],
                   "connected":false,
                   "description":"Unknown Status"}}]}

The response is exactly what I expect for this query. 1 page. 30 totalCount.

Requesting the next page returns this response:

{"links":[],"count":0,"totalCount":30,"endpointList":[]}

This looks good to me as well.

The issue is with ExtJS. When my store's pageSize == the totalCount property returned by my request the grid spins; loading infinitely or until the response's totalCount != pageSize.

ExtJS throws an exception during this infinite load (ext-all-debug.js:104512):

Uncaught Error: NotFoundError: DOM Exception 8

Surrounding code from Ext.view.NodeCache.scroll:

scroll: function(newRecords, direction, removeCount) {
    var me = this,
        elements = me.elements,
        recCount = newRecords.length,
        i, el, removeEnd,
        newNodes,
        nodeContainer = me.view.getNodeContainer(),
        frag = document.createDocumentFragment();


    if (direction == -1) {
        for (i = (me.endIndex - removeCount) + 1; i <= me.endIndex; i++) {
            el = elements[i];
            delete elements[i];
            el.parentNode.removeChild(el);
        }
        me.endIndex -= removeCount;


        newNodes = me.view.bufferRender(newRecords, me.startIndex -= recCount);
        for (i = 0; i < recCount; i++) {
            elements[me.startIndex + i] = newNodes[i];
            frag.appendChild(newNodes[i]);
        }
        nodeContainer.insertBefore(frag, nodeContainer.firstChild);
    }


    else {
        removeEnd = me.startIndex + removeCount;
        for (i = me.startIndex; i < removeEnd; i++) {
            el = elements[i];
            delete elements[i];
            el.parentNode.removeChild(el);
        }
        me.startIndex = i;


        newNodes = me.view.bufferRender(newRecords, me.endIndex + 1);
        for (i = 0; i < recCount; i++) {
            elements[me.endIndex += 1] = newNodes[i];
            frag.appendChild(newNodes[i]);
            Uncaught Error: NotFoundError: DOM Exception 8 (repeated 5 times)
        }
        nodeContainer.appendChild(frag);
    }

    me.count = me.endIndex - me.startIndex + 1;
}

My grid:

/**
 * TODO Known Defects: If the size of the result set returned by the search
 * filter == pageSize then the grid loads forever. The requests and responses
 * look correct, but an exception is thrown in extjs
 */

Ext.Loader.setPath('Ext.ux', 'resources/js/extjs/examples/ux');
Ext.require([ 'Ext.grid.*', 'Ext.data.*', 'Ext.util.*',
'Ext.grid.plugin.BufferedRenderer', 'Ext.ux.form.SearchField' ]);

Ext.onReady(function() {

    Ext.QuickTips.init();

    Ext.define('Endpoint', {

        extend : 'Ext.data.Model',
        fields : [ {
            name : 'deviceType',
            type : 'string',
        }, {
            name : 'name',
            type : 'string',
        }, {
            name : 'managed',
            type : 'boolean',
        }, {
            name : 'updateInterval',
            type : 'int',
        }, {
            name : 'timeout',
            type : 'int',
        }, {
            name : 'checkedAt',
            type : 'string',
        }, {
            name : 'currentStatus',
            mapping : 'currentStatus.description',
            type : 'string',
        } ],
    });

    var store = new Ext.create('Ext.data.Store', {
        id : 'store',
        model : 'Endpoint',

        autoLoad : true,
        buffered : true,
        pageSize : 30,
        leadingBufferZone : 5,
        remoteFilter : true,
        remoteSort : true,

        sorters : [ {
            property : 'checkedAt',
            direction : 'desc'
        } ],

        listeners : {
            totalcountchange : onStoreSizeChange
        },

        proxy : {
            type : 'rest',
            url : '/endpointmanager/endpoints.json',
            simpleSortMode : true,
            filterParam : 'query',
            encodeFilters : function(filters) {
                return filters[0].value;
            },
            reader : {
                type : 'json',
                root : 'endpointList',
                totalProperty : 'totalCount',
            },
            writer : {
                type : 'json',
            },
        },
    });

    function onStoreSizeChange() {
        grid.down('#status').update({
            count : store.getTotalCount()
        });
    }

    var grid = Ext
            .create(
                    'Ext.grid.Panel',
                    {

                        store : store,
                        renderTo : 'endpoint-grid',
                        height : 600,
                        forceFit : true,
                        columnLines : true,
                        stripeRows : true,
                        loadMask : true,
                        multiselect : true,
                        viewConfig : {
                            trackOver : false,
                            emptyText : '<h1 style="margin:20px">No matching endpoints found</h1>'
                        },
                        selModel : {
                            pruneRemoved : false
                        },                              

                        columns : [ {
                            xtype : 'checkcolumn',
                            header : 'Managed',
                            dataIndex : 'managed',
                            width : 100,
                            editor : {
                                xtype : 'checkbox',
                                cls : 'x-grid-checkheader-editor'
                            },
                        }, {
                            header : 'Checked At',
                            dataIndex : 'checkedAt',
                            renderer : dateConverter,
                            width : 160,
                        }, {
                            header : 'Device Type',
                            dataIndex : 'deviceType',
                            width : 160,
                        }, {
                            header : 'Name',
                            dataIndex : 'name',
                            width : 160,
                            flex : 1,
                        }, {
                            header : 'Update Interval (sec)',
                            dataIndex : 'updateInterval',
                            width : 160,
                            editor : {
                                xtype : 'numberfield',
                                allowBlank : false,
                                minValue : 10,
                                maxValue : 2600000
                            },
                        }, {
                            header : 'Timeout (sec)',
                            dataIndex : 'timeout',
                            width : 160,
                            editor : {
                                xtype : 'numberfield',
                                allowBlank : false,
                                minValue : -1,
                                maxValue : 3600,
                            }
                        }, {
                            header : 'Status',
                            dataIndex : 'currentStatus',
                            width : 160,
                        } ],

                        dockedItems : [
                                {
                                    xtype : 'toolbar',
                                    dock : 'top',
                                    items : [
                                            {
                                                width : 400,
                                                fieldLabel : 'Search',
                                                labelWidth : 50,
                                                xtype : 'searchfield',
                                                store : store
                                            },
                                            '->',
                                            {
                                                xtype : 'component',
                                                itemId : 'status',
                                                tpl : 'Matching Endpoints: {count}',
                                                style : 'margin-right:5px'
                                            } ]

                                },
                                {
                                    xtype : 'toolbar',
                                    cls : 'listFooter',
                                    dock : 'bottom',
                                    items : [
                                            {
                                                xtype : 'tbfill'
                                            },
                                            {
                                                text : 'remove',
                                                cls : 'gridButton',
                                                iconCls : 'icon-delete',
                                                handler : function() {
                                                    var selection = grid
                                                            .getView()
                                                            .getSelectionModel()
                                                            .getSelection()[0];
                                                    if (selection) {
                                                        store
                                                                .remove(selection);
                                                    }
                                                }
                                            },
                                            '-',
                                            {
                                                text : 'edit',
                                                cls : 'gridButton',
                                                iconCls : 'icon-edit',
                                                handler : function() {
                                                    store
                                                            .insert(
                                                                    0,
                                                                    new Endpoint());
                                                },
                                            },
                                            '-',
                                            {
                                                text : 'inline',
                                                cls : 'gridButton',
                                                iconCls : 'icon-add',
                                                handler : function() {
                                                    store
                                                            .insert(
                                                                    0,
                                                                    new Endpoint());
                                                },
                                            },
                                            '-',
                                            {
                                                text : 'add',
                                                cls : 'gridButton',
                                                iconCls : 'icon-add',
                                                handler : function() {
                                                    window.location = 'endpointManagementAdd';
                                                },
                                            } ],
                                } ],
                    });

    var task = {
        run : function() {
            store.load();
        },
        interval : 30000
    };

    // kick-off refresh task
    Ext.TaskManager.start(task);

});

function dateConverter(data, cell, record, rowIndex, columnIndex, store) {
  if (data == "") {
return;
  }

  var dt = new Date(parseInt(data));

  return dt.toLocaleString();
};

Has anyone encountered this problem before? I am wondering if there is a way to work around the exception thrown in ExtJS and whether or not there is something abnormal which I am doing to trigger this behavior. Any insight much appreciated. The grid works excellent aside from this one defect.

2
Sorry I don't have a work around, but the infinite grid in the latest ExtJS release is really buggy.. I cannot seem to find the thread now, but I saw a similar issue reported on the ExtJS forums. I would try using an older version of the Framework to see if you are still having issuesAndrew Lapham
Thanks for the heads up. I'll do some digging and try an older release if I can't figure out a work around...Ethan Anderson
From my experience there is no version of ExtJS that has a reliable infinite grid. I will say each release has gotten slightly better, but I am hoping what mars-red said about 4.2.2 holds to be true.FoxMulder900

2 Answers

2
votes

I'm not sure if this is appropriate for an "answer" but I don't have enough reputation to add a comment as of this moment. I ran into similar problems with 4.2.1 and found that they are fixed in 4.2.2. As of right now you need premium support with them to get that version so that may not help you. I haven't tried an older version, I'm using another 3rd party library that requires a 4.2 version of Ext JS.

Anyway, I posted in the Sencha forums about a problem I was having with a DOM exception and just generally strange behavior of the loading indicator. When I found that 4.2.2 fixed my problems, I tried this scenario with the total count being sent back from the server being the same as the data store page size and could not reproduce the problem.

0
votes

You can try using an override like this

Ext.define('NodeCacheOverride', {
override: 'Ext.view.NodeCache',
/**
 * Appends/prepends records depending on direction flag
 * @param {Ext.data.Model[]} newRecords Items to append/prepend
 * @param {Number} direction `-1' = scroll up, `0` = scroll down.
 * @param {Number} removeCount The number of records to remove from the end. if scrolling
 * down, rows are removed from the top and the new rows are added at the bottom.
 */
scroll: function (newRecords, direction, removeCount) {
    var me = this,
        elements = me.elements,
        recCount = newRecords.length,
        i, el, removeEnd,
        newNodes,
        nodeContainer = me.view.getNodeContainer(),
        frag = document.createDocumentFragment();

    // Scrolling up (content moved down - new content needed at top, remove from bottom)
    if (direction == -1) {
        for (i = (me.endIndex - removeCount) + 1; i <= me.endIndex; i++) {
            el = elements[i];
            delete elements[i];
            el.parentNode.removeChild(el);
        }
        me.endIndex -= removeCount;

        // grab all nodes rendered, not just the data rows
        newNodes = me.view.bufferRender(newRecords, me.startIndex -= recCount);
        for (i = 0; i < recCount; i++) {
            elements[me.startIndex + i] = newNodes[i];
            frag.appendChild(newNodes[i]);
        }
        nodeContainer.insertBefore(frag, nodeContainer.firstChild);
    }

    // Scrolling down (content moved up - new content needed at bottom, remove from top)
    else {


        if(me.count == me.endIndex + 1) return; // Override modification

        removeEnd = me.startIndex + removeCount;
        for (i = me.startIndex; i < removeEnd; i++) {
            el = elements[i];
            delete elements[i];
            el.parentNode.removeChild(el);
        }
        me.startIndex = i;
        // grab all nodes rendered, not just the data rows
        newNodes = me.view.bufferRender(newRecords, me.endIndex + 1);
        for (i = 0; i < recCount; i++) {
            elements[me.endIndex += 1] = newNodes[i];
            frag.appendChild(newNodes[i]);
        }
        nodeContainer.appendChild(frag);
    }
    // Keep count consistent.
    me.count = me.endIndex - me.startIndex + 1;
}

});

Get sure to include it before declaring your view. I've not been able to reproduce the issue so it's not fully tested, so if it doesn't work out of the box, I will advise you to tweak a little until it handles the exception that hangs the rest of your rendering operation.

Hope this helps you to find the right solution.