ExtJS 6.5.3 and older versions has a bug. If you are using ‘Ext.grid.plugin.RowExpander’ plugin, the expanded rows will be collapsed after grid’s store reload/sort, on any refresh of the grid view.
You can fix it using the following override of the ‘Ext.grid.Row’ class. New logic is based on store’s records ids, so in case of the auto-generated ids (not server side constant ids) the fix may work incorrect. Looks like the same problem was in the older versions ExtJS Modern.
Ext.define('overrides.grid.Row', { override: 'Ext.grid.Row', refresh: function (context) { console.log('Refresh'); var me = this, cells = me.cells, body = me.getBody(), len = cells.length, expandField = me.getExpandedField(), grid = me.getParent(), sm = grid.getSelectable(), selection = sm.getSelection(), isCellSelection = selection.isCells || selection.isColumns, i, visibleIndex, cell, record, recordsExpanded; // Allows cells/body to know we are bulk updating so they can avoid // things like calling record.getData(true) multiple times. me.refreshContext = context = me.beginRefresh(context); record = context.record; me.syncDirty(record); for (i = 0, visibleIndex = 0; i < len; ++i) { cell = cells[i]; if (!context.summary || !cell.getColumn().getIgnore()) { if (cell.getRecord() === record) { cell.refresh(context); } else { cell.refreshContext = context; cell.setRecord(record); cell.refreshContext = null; } if (isCellSelection) { cell.toggleCls(grid.selectedCls, sm.isCellSelected(me._recordIndex, visibleIndex)); } } // Cell and column selection work on visible index. if (!cell.isHidden()) { visibleIndex++; } } context.cell = context.column = context.dataIndex = context.scope = null; if (body) { body.refreshContext = context; if (body.getRecord() === record) { body.updateRecord(record); } else { body.setRecord(record); } body.refreshContext = null; // If the plugin knows that the record contains an expanded flag // ensure our state is synchronized with our record. // Maintainer: We are testing the result of the assignment of expandedField // in order to avoid a messy, multiple level if...else. if (expandField) { me.setCollapsed(!record.get(expandField)); } else { recordsExpanded = grid.$recordsExpanded || (grid.$recordsExpanded = {}); if (grid.hasRowExpander) { me.setCollapsed(!recordsExpanded[record.getId()]); } } } me.refreshContext = null; }, updateCollapsed: function (collapsed) { console.log('Update'); var me = this, body = me.getBody(), grid = me.getParent(), record = me.getRecord(), expandField = me.getExpandedField(), expandedCls = me.expandedCls, expanderCell = me.expanderCell, recordsExpanded; // Set state correctly before any other code executes which may read this. if (record) { // We have to track the state separately, if we are not using a record // field to track expanded state. if (expandField) { record.set(expandField, !collapsed); } else { recordsExpanded = grid.$recordsExpanded || (grid.$recordsExpanded = {}); if (collapsed) { delete recordsExpanded[record.getId()]; } else { recordsExpanded[record.getId()] = true; } } } if (expanderCell) { expanderCell.setCollapsed(collapsed); } if (body) { if (collapsed) { body.hide(); me.removeCls(expandedCls); } else { body.show(); me.addCls(expandedCls); } } } });
And the working fiddle sample: