Skip to content

Instantly share code, notes, and snippets.

@andrewsantarin
Last active February 4, 2020 05:30
Show Gist options
  • Save andrewsantarin/18355256af37aea58711a8d12e1183f6 to your computer and use it in GitHub Desktop.
Save andrewsantarin/18355256af37aea58711a8d12e1183f6 to your computer and use it in GitHub Desktop.
Grid-in-Grid for Ext.js v5.0.1
[
{
"id": 4,
"no": "001122",
"description": "Order description",
"customerId": 1,
"parentId": 4
},
{
"id": 41,
"no": "001122-E",
"description": "Order description",
"customerId": 1,
"parentId": 4
},
{
"id": 5,
"no": "334455",
"description": "Order description",
"customerId": 2,
"parentId": 5
},
{
"id": 51,
"no": "334455-E",
"description": "Order description",
"customerId": 2,
"parentId": 5
},
{
"id": 6,
"no": "667788",
"description": "Order description",
"customerId": 2,
"parentId": ""
},
{
"id": 7,
"no": "889900",
"description": "Order description",
"customerId": 1
}
]
[
{
"id": 1,
"no": "112233",
"description": "Order description",
"customerId": 1,
"parentId": 1
},
{
"id": 11,
"no": "112233-A",
"description": "Order description",
"customerId": 1,
"parentId": 1
},
{
"id": 12,
"no": "112233-B",
"description": "Order description",
"customerId": 1,
"parentId": 1
},
{
"id": 2,
"no": "445566",
"description": "Order description",
"customerId": 2,
"parentId": 2
},
{
"id": 21,
"no": "445566-C",
"description": "Order description",
"customerId": 2,
"parentId": 2
},
{
"id": 22,
"no": "445566-D",
"description": "Order description",
"customerId": 2,
"parentId": 2
},
{
"id": 3,
"no": "778899",
"description": "Order description",
"customerId": 1,
"parentId": null
}
]
[
{
"id": 4,
"no": "001122",
"description": "Order description",
"customerId": 1,
"children": [
{
"id": 41,
"no": "001122-E",
"description": "Order description",
"customerId": 1
}
],
},
{
"id": 5,
"no": "334455",
"description": "Order description",
"customerId": 2,
"children": [
{
"id": 51,
"no": "334455-E",
"description": "Order description",
"customerId": 2
}
],
},
{
"id": 6,
"no": "667788",
"description": "Order description",
"customerId": 2
},
{
"id": 7,
"no": "889900",
"description": "Order description",
"customerId": 1
}
]
[
{
"id": 1,
"no": "112233",
"description": "Order description",
"customerId": 1,
"children": [
{
"id": 11,
"no": "112233-A",
"description": "Order description",
"customerId": 1
},
{
"id": 12,
"no": "112233-B",
"description": "Order description",
"customerId": 1
}
]
},
{
"id": 2,
"no": "445566",
"description": "Order description",
"customerId": 2,
"children": [
{
"id": 21,
"no": "445566-C",
"description": "Order description",
"customerId": 2
},
{
"id": 22,
"no": "445566-D",
"description": "Order description",
"customerId": 2
}
]
},
{
"id": 3,
"no": "778899",
"description": "Order description",
"customerId": 1
}
]
Ext.application({
name: 'Fiddle',
loaded: false,
added: false,
launch: function () {
var me = this;
var storeConfig = {
title: 'Orders',
fields: [
'id',
'no',
'customerId',
'description'
],
groupField: 'customerId',
proxy: {
type: 'memory',
reader: {
type: 'json'
}
}
};
var columns = [
{
text: 'ID',
dataIndex: 'id'
},
{
text: 'Order Number',
dataIndex: 'no'
},
{
text: 'Order Description',
dataIndex: 'description',
flex: 1
}
];
var store = Ext.create('Ext.data.Store', Ext.merge({}, storeConfig, {
listeners: {
datachanged: function updateStores(thisStore) {
var records = thisStore.getRange();
var childRecords = records.filter(function (record) {
var parentId = record.get('parentId');
var id = record.get('id');
return parentId === id
|| parentId === ''
|| parentId == null;
});
me.grid.store.loadData(childRecords);
}
}
}));
var grid = new GridInGrid({
columns: columns,
store: Ext.create('Ext.data.Store', storeConfig),
getSubGridViewConfig: function (rowNode, record, expandRow, eOpts) {
var _id = record.get('id');
var records = store.getRange();
var childStore = Ext.create('Ext.data.Store', storeConfig);
var childData = records.filter(function (record) {
var parentId = record.get('parentId');
var id = record.get('id');
return parentId === _id
&& parentId !== id;
});
childStore.loadData(childData);
var config = {
columns: columns,
store: childStore
};
return config;
},
getSubGridViewConfigChildren: function (record, store) {
var children = store.getRange();
return children;
},
shouldRowExpand: function requireRows(record, rowIndex, rowParams) {
var _id = record.get('id');
var _records = store.getRange();
var records = _records.filter(function (record) {
var parentId = record.get('parentId');
var id = record.get('id');
return parentId === _id
&& parentId !== id;
});
var hasRecords = records.length > 0;
return hasRecords;
},
features: [
{
ftype: 'grouping'
}
],
height: '100%',
width: '100%',
title: 'Orders'
});
this.grid = grid;
window.grid = grid;
window.store = store;
var app = this;
this.loadOrdersButton = Ext.create('Ext.Button', {
text: 'Load initial orders',
renderTo: Ext.getBody(),
handler: function() {
if (app.loaded) {
return;
}
store.loadData(orders);
app.loaded = true;
app.resetButton.setDisabled(false);
this.setDisabled(true);
}
});
this.loadNewOrdersButton = Ext.create('Ext.Button', {
text: 'Add more orders',
renderTo: Ext.getBody(),
handler: function() {
if (app.added || !app.loaded) {
return;
}
var data = store.getData();
var orders = data.items.map(function (o) {
return o.data;
});
store.loadData(orders.concat(newOrders));
app.added = true;
app.resetButton.setDisabled(false);
this.setDisabled(true);
}
});
this.resetButton = Ext.create('Ext.Button', {
text: 'Reset',
renderTo: Ext.getBody(),
disabled: true,
handler: function() {
if (!app.loaded) {
return;
}
store.removeAll();
app.loaded = false;
app.loadOrdersButton.setDisabled(false);
app.added = false;
app.loadNewOrdersButton.setDisabled(false);
this.setDisabled(true);
}
})
var win = Ext.create('Ext.window.Window', {
title: 'Grid in Grid',
titleAlign: 'center',
resizable: true,
maximizable: true,
layout: 'fit',
height: 600,
width: 600,
items: [grid]
});
win.show();
}
});
Ext.application({
name: 'Fiddle',
loaded: false,
added: false,
launch: function () {
var me = this;
var storeConfig = {
title: 'Orders',
fields: [
'id',
'no',
'customerId',
'description'
],
groupField: 'customerId',
proxy: {
type: 'memory',
reader: {
type: 'json'
}
}
};
var columns = [
{
text: 'ID',
dataIndex: 'id'
},
{
text: 'Order Number',
dataIndex: 'no'
},
{
text: 'Order Description',
dataIndex: 'description',
flex: 1
}
];
var store = Ext.create('Ext.data.Store', Ext.merge({}, storeConfig, {
listeners: {
datachanged: function updateStores(thisStore) {
var records = thisStore.getRange();
var childRecords = records.filter(function (record) {
var parentId = record.get('parentId');
var id = record.get('id');
return parentId === id
|| parentId === ''
|| parentId == null;
});
me.grid.store.loadData(childRecords);
}
}
}));
var grid = new GridInGrid({
columns: columns,
store: Ext.create('Ext.data.Store', storeConfig),
getSubGridViewConfig: function (rowNode, record, expandRow, eOpts) {
var children = record.get('children');
var store = Ext.create('Ext.data.Store', Ext.apply({}, storeConfig, {
data: children
}));
var viewConfig = {
columns: columns,
store: store
};
return viewConfig;
},
getSubGridViewConfigChildren: function (record, store) {
var children = record.get('children');
return children;
},
features: [
{
ftype: 'grouping'
}
],
height: '100%',
width: '100%',
title: 'Orders'
});
this.grid = grid;
window.grid = grid;
window.store = store;
var app = this;
this.loadOrdersButton = Ext.create('Ext.Button', {
text: 'Load initial orders',
renderTo: Ext.getBody(),
handler: function() {
if (app.loaded) {
return;
}
store.loadData(orders);
app.loaded = true;
app.resetButton.setDisabled(false);
this.setDisabled(true);
}
});
this.loadNewOrdersButton = Ext.create('Ext.Button', {
text: 'Add more orders',
renderTo: Ext.getBody(),
handler: function() {
if (app.added || !app.loaded) {
return;
}
var data = store.getData();
var orders = data.items.map(function (o) {
return o.data;
});
store.loadData(orders.concat(newOrders));
app.added = true;
app.resetButton.setDisabled(false);
this.setDisabled(true);
}
});
this.resetButton = Ext.create('Ext.Button', {
text: 'Reset',
renderTo: Ext.getBody(),
disabled: true,
handler: function() {
if (!app.loaded) {
return;
}
store.removeAll();
app.loaded = false;
app.loadOrdersButton.setDisabled(false);
app.added = false;
app.loadNewOrdersButton.setDisabled(false);
this.setDisabled(true);
}
})
var win = Ext.create('Ext.window.Window', {
title: 'Grid in Grid',
titleAlign: 'center',
resizable: true,
maximizable: true,
layout: 'fit',
height: 600,
width: 600,
items: [grid]
});
win.show();
}
});
.ux-row-expander-hidden .x-grid-row-expander {
visibility: hidden;
}
/**
* Grid-in-Grid for single-level nested grids.
*
* @class GridInGrid
* @param {Partial<GridInGrid>} options Additional options.
*/
var GridInGrid = Ext.define('grid.GridInGrid', {
extend: 'Ext.grid.Panel',
alias: 'widget.gridingrid',
/**
* (optional) The CSS class to assign to the expandable subgrid content.
*
* @default "ux-row-expander-box"
*/
rowBodyTplCss: 'ux-row-expander-box',
/**
* (optional) The CSS class to assign to the row subgrid toggler when no subgrid data is available.
*
* @default "ux-row-expander-hidden"
*/
rowHideExpanderCss: 'ux-row-expander-hidden',
/**
* (optional) A callback which generates the configuration for the subgrid. If not specified, the subgrid will not be rendered.
*
* @default null
* @param {HTMLElement} rowNode The `<tr>` element which owns the expanded row.
* @param {Ext.data.Model} record The record providing the data for the row as modeled for the store.
* @param {HTMLElement} expandRow The `<tr>` element containing the expanded data.
* @param {Object} eOpts Options passed to the subgrid.
* @returns {Partial<Ext.grid.Panel>} A configuration object for the subgrid.
*/
getSubGridViewConfig: null,
/**
* (optional) A callback which generates a custom list of row data for the subgrid. If not specified, the subgrid will use the parent row data's `children` attribute.
*
* @default null
* @param {Ext.data.Model} record The individual row data entry context from the maingrid.
* @param {Ext.data.Store} store The row data's store. Can also be accessed from `record.store`.
* @returns {Ext.data.Store[]} A list of row data.
*/
getSubGridViewConfigChildren: null,
/**
* (optional) A callback which determines, **in context of one row and its subgrid**:
*
* - whether to show or hide the toggling control which expands the row subgrid area when the control is clicked.
* - whether to expand or collapse the row subgrid area when the toggling control is clicked.
* - whether to expand or collapse the row subgrid area when the `[Enter]` key is pressed while the row is highlighted.
*
* If not specified, the subgrid will only be toggled when the row contains children data (see `getSubGridViewConfigChildren(record, store)`).
*
* @default null
* @param {Ext.data.Model} record The record corresponding to the current row.
* @param {Number} index The row index.
* @param {Object} rowParams - **DEPRECATED.** For row body, use the `getAdditionalData` method of the rowbody feature.
* @param {Ext.data.Store} store The store this grid is bound to.
* @returns {Boolean} An indicator which determines whether to show or hide the individual row subgrids.
*/
shouldRowExpand: null,
/**
* @see https://fiddle.sencha.com/#fiddle/jqi&view/editor
*/
initComponent: function GridInGrid_initComponent() {
// NOTE: It's necessary to use the extended RowExpander plugin to use the smart toggler element show-hide condition.
var moduleName = 'grid.plugin.RowContentExpander';
var me = this;
me.requires = mergeRequires(me.requires, [moduleName]);
me.plugins = mergePlugins(me.plugins, {
rowcontentexpander: Ext.create(moduleName, {
rowBodyTpl: '<div class="' + me.rowBodyTplCss + '"></div>',
shouldToggleRow: function GridInGrid_shouldToggleRow(plugin, rowIndex, record) {
// HACK: Since `rowParams` is said to be deprecated, `null` might as well be used, which is the value seen during testing `getRowClass`.
var rowParams = null;
var store = me.getStore();
var isExpandable = me.rowIsExpandable(record, rowIndex, rowParams, store);
return isExpandable;
}
})
});
// TODO: Improve the flexibility of this segment.
me.viewConfig = {
getRowClass: function GridInGrid_getRowClass(record, rowIndex, rowParams, store) {
var isExpandable = me.rowIsExpandable(record, rowIndex, rowParams, store);
if (!isExpandable) {
return me.rowHideExpanderCss;
}
},
listeners: {
expandbody: function GridInGrid_handleExpandBody(rowNode, record, expandRow, eOpts) {
var element = Ext.get(expandRow)
.child('.x-grid-cell-rowbody')
.child('.x-grid-rowbody')
.child('.' + me.rowBodyTplCss);
var container = element.child('.x-panel', true);
if (container != null) {
return;
}
var viewConfig = typeof me.getSubGridViewConfig === 'function' ? me.getSubGridViewConfig(rowNode, record, expandRow, eOpts) : null;
if (viewConfig == null) {
return;
}
var children = typeof me.getSubGridViewConfigChildren === 'function' ? me.getSubGridViewConfigChildren(record, viewConfig.store) : record.get('children');
if (children == null || children.length === 0) {
return;
}
// Prevent event bubbling errors.
// https://stackoverflow.com/questions/29717913/extjs-error-in-nested-grids-cannot-read-property-isgroupheader-of-null
var swallowedEvents = [
'contextmenu',
'click',
'dblclick',
'mousedown',
'mouseup',
'mouseover',
'mouseout',
'mousemove'
];
// If a nested grid is necessary, perhaps recursive component creation is necessary.
var subGrid = Ext.create('Ext.grid.Panel', viewConfig);
subGrid.render(element);
subGrid.getEl().swallowEvent(swallowedEvents);
}
}
};
me.callParent(arguments);
},
rowIsExpandable: function GridInGrid_rowIsExpandable(record, rowIndex, rowParams, store) {
var me = this;
var children = typeof me.getSubGridViewConfigChildren === 'function' ? me.getSubGridViewConfigChildren(record, store) : record.get('children');
var hasChildren = children != null && children.length > 0;
var isExpandable = typeof me.shouldRowExpand === 'function' ? me.shouldRowExpand(record, rowIndex, rowParams, store) : hasChildren;
return isExpandable;
},
});
/**
* Row Expander with conditional toggling per row.
*
* @class RowContentExpander
* @param {Partial<RowContentExpander>} options Additional options.
*/
var RowContentExpander = Ext.define('grid.plugin.RowContentExpander', {
extend: 'Ext.grid.plugin.RowExpander',
alias: 'plugin.rowcontentexpander',
/**
* Assigns grid, the plugin name and calls the parent instance.
*/
init: function RowContentExpander_init(grid) {
var me = this;
// Provide the module name so that it can be searched in other
me.pluginModuleName = 'App.grid.plugin.RowContentExpander';
me.grid = grid;
me.callParent(arguments);
},
/**
* Handles row expansion and collapse.
*
* @see https://fiddle.sencha.com/#fiddle/10ta&view/editor
*/
toggleRow: function RowContentExpander_toggleRow(index, record) {
var me = this;
var isToggleable = typeof me.shouldToggleRow === 'function' ? me.shouldToggleRow(me, index, record) : true;
if (!isToggleable) {
return;
}
me.callParent(arguments);
}
});
/**
* Merges a plugin map into a list of plugins.
*
* @param {*} [plugins] The original list of plugins.
* @param {*} [pluginMap] An object map of additional plugins to add.
* @returns {(Ext.enums.Plugin | Ext.plugin.Abstract | Object)[]} A merged list of instantiatable plugins.
*/
function mergePlugins(plugins, pluginMap) {
var finalPlugins = plugins;
if (finalPlugins == null) {
finalPlugins = [];
}
if (Ext.isObject(finalPlugins)) {
finalPlugins = [finalPlugins];
}
if (!Ext.isArray(finalPlugins)) {
finalPlugins = [finalPlugins];
}
Ext.Object.getKeys(pluginMap).forEach(function (key) {
var isFound = finalPlugins.filter(function (plugin) {
if (plugin == null) {
return false;
}
return plugin.ptype === key || plugin === key;
}).length > 0;
if (!isFound) {
finalPlugins.push(pluginMap[key]);
}
});
return finalPlugins;
}
/**
* Merges two "requires" lists together.
*
* @param {String[]} [requires] The original list of modules.
* @param {String[]} [newRequires] An additional list of modules to add.
* @returns {String[]} A merged list of required modules.
*/
function mergeRequires(requires, newRequires) {
var oldRequires = Ext.isArray(requires) ? requires : [];
if (newRequires == null) {
return oldRequires;
}
if (!Ext.isArray(newRequires)) {
return oldRequires;
}
return Ext.Array.merge(oldRequires, newRequires);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment