I'm currently using Bryntum grid version 5.4.0 with JavaScript implementation, and I have integrated it as a component in my Vue.js page. I have set the props for grid columns, data, and features.
I am attaching the sample video for the requirements I want to achieve. I want to show/hide the two grids with the click of two individual button clicks. Refer to the sample video for the same.
I wondered if there's a way to implement it using the Bryntm grid. Could you provide guidance or suggestions on how to achieve this functionality?
By your description and the video that you shared, I believe you'll find this demo useful https://bryntum.com/products/grid/examples/nested-grid/ which works very similar to what you're looking for (having a Grid inside another Grid).
By your description and the video that you shared, I believe you'll find this demo useful https://bryntum.com/products/grid/examples/nested-grid/ which works very similar to what you're looking for (having a Grid inside another Grid).
Please let me know your thoughts about that one.
Hello Marcio,
Thank you for your prompt response. Upon reviewing the demo, I observed that it supports nested grids. However, in our scenario, we plan to have two independent grids that will open separately upon clicking two distinct buttons. These two grids will not be nested but will be displayed consecutively, one below the other. Could you please revisit the video for additional clarity on this specific configuration?
So, to confirm, as far as I can see in the video, the grids will be displayed "inside" the main grid, in a row, not below the Grid, is that correct?
A possible approach would be to have a container that will have both grids and then programmatically control the display/not display of each grid, which will be controlled by the button clicks.
So, to confirm, as far as I can see in the video, the grids will be displayed "inside" the main grid, in a row, not below the Grid, is that correct?
A possible approach would be to have a container that will have both grids and then programmatically control the display/not display of each grid, which will be controlled by the button clicks.
Thanks for your reply. We referred to the provided link. Could you please share an example for the same?
It would be helpful if we could get one to move further with the requirement.
widget : {
type : 'container',
layout : 'vbox',
items : {
grid : {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
autoHeight : true, // Grid resizes to fit all rows
columns : [
{ text : 'Project', field : 'name', flex : 1 },
{ text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
{ text : 'Attested', field : 'attested', width : 100, type : 'check' }
],
bbar : [ // Bottom toolbar
// Add button which adds a row to the expanded grid and starts editing it
{
text : 'Add',
icon : 'b-icon-add',
onClick : ({ source }) => {
const
grid = source.up('grid'),
expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
[newRecord] = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });
grid.startEditing(newRecord);
}
}, '->', {
// Button that sets all rows as "attested"
text : 'Attest all',
icon : 'b-icon-check',
onClick : ({ source }) => {
const { store } = source.up('grid');
store.forEach(r => r.attested = true);
}
}
]
},
grid2 : {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
hidden : true,
autoHeight : true, // Grid resizes to fit all rows
columns : [
{ text : 'Project', field : 'name', flex : 1 },
{ text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
{ text : 'Attested', field : 'attested', width : 100, type : 'check' }
],
bbar : [ // Bottom toolbar
// Add button which adds a row to the expanded grid and starts editing it
{
text : 'Add',
icon : 'b-icon-add',
onClick : ({ source }) => {
const
grid = source.up('grid'),
expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
[newRecord] = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });
grid.startEditing(newRecord);
}
}, '->', {
// Button that sets all rows as "attested"
text : 'Attest all',
icon : 'b-icon-check',
onClick : ({ source }) => {
const { store } = source.up('grid');
store.forEach(r => r.attested = true);
}
}
]
}
}
}
Hi Tasnim,
Thanks for your reply. We referred to the provided code and attempted to implement it in the example section under the rowexpander and nested grid, but it didn't work for us. We believe we might have missed something. Could you please provide a working example so that it would be easier for us to proceed further?
import { DataGenerator, RandomGenerator, DateHelper, Grid, GridRowModel, Store, StringHelper } from '../../build/grid.module.js?473556';
import shared from '../_shared/shared.module.js?473556';
const randomGenerator = new RandomGenerator();
export const getEmployees = () => {
const data = DataGenerator.generateData(25);
for (const row of data) {
DateHelper.add(row.start, row.id * 2, 'month', false);
row.email = `${row.name.toLowerCase().replaceAll(' ', '.')}@example.com`;
}
return data;
};
export const getTimeRows = (employees) => {
const
tasks = DataGenerator.generateEvents({ viewStartDate : new Date(), viewEndDate : new Date(), nbrResources : 1, nbrEvents : 35 }).events.map(e => e.name),
rows = [];
let id = 0;
for (const employee of employees) {
for (let i = 0; i < Math.ceil(Math.random() * 10); i++) {
id += 1;
rows.push({
id,
employeeId : employee.id,
name : randomGenerator.fromArray(tasks),
hours : Math.ceil(Math.random() * 100),
attested : Math.random() > 0.5
});
}
}
return rows;
};
// These two model classes and two stores uses the built-in data-relations feature to connect related records to each other.
class Employee extends GridRowModel {
static fields = ['id', 'firstName', 'name', 'start', 'email'];
// Example how to set up a "calculated" field
get unattested() {
return this.timeRows.reduce((acc, r) => acc + (r.attested ? 0 : 1), 0);
}
// Example how to set up a "calculated" field
get totalTime() {
return this.timeRows.reduce((acc, r) => acc + r.hours, 0);
}
}
class TimeRow extends GridRowModel {
static fields = ['id', 'employeeId', 'project', 'hours', 'attested'];
// The relations config, will set up the relationship between the two stores and model classes
static relations = {
employee : {
foreignKey : 'employeeId', // The id of the "other" record
foreignStore : 'employeeStore', // Must be set on the record's store
relatedCollectionName : 'timeRows' // The field in which these records will be accessible from the "other" record
}
};
}
const employeeStore = new Store({
modelClass : Employee,
data : getEmployees()
});
const timeRowStore = new Store({
modelClass : TimeRow,
employeeStore, // Must be set, see relations config above
data : getTimeRows(employeeStore.records)
});
const grid = new Grid({
appendTo : 'container',
features : {
rowExpander : {
// The widget config declares what type of Widget will be created on each expand
renderer({ record }) {
return {
type : 'container',
layout : 'vbox',
items : {
grid : {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
autoHeight : true, // Grid resizes to fit all rows
columns : [
{ text : 'Project', field : 'name', flex : 1 },
{ text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
{ text : 'Attested', field : 'attested', width : 100, type : 'check' }
],
bbar : [ // Bottom toolbar
// Add button which adds a row to the expanded grid and starts editing it
{
text : 'Add',
icon : 'b-icon-add',
onClick : ({ source }) => {
const
grid = source.up('grid'),
expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
[newRecord] = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });
grid.startEditing(newRecord);
}
}, '->', {
// Button that sets all rows as "attested"
text : 'Attest all',
icon : 'b-icon-check',
onClick : ({ source }) => {
const { store } = source.up('grid');
store.forEach(r => r.attested = true);
}
}
]
},
grid2 : {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
hidden : true,
autoHeight : true, // Grid resizes to fit all rows
columns : [
{ text : 'Project', field : 'name', flex : 1 },
{ text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
{ text : 'Attested', field : 'attested', width : 100, type : 'check' }
],
bbar : [ // Bottom toolbar
// Add button which adds a row to the expanded grid and starts editing it
{
text : 'Add',
icon : 'b-icon-add',
onClick : ({ source }) => {
const
grid = source.up('grid'),
expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
[newRecord] = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });
grid.startEditing(newRecord);
}
}, '->', {
// Button that sets all rows as "attested"
text : 'Attest all',
icon : 'b-icon-check',
onClick : ({ source }) => {
const { store } = source.up('grid');
store.forEach(r => r.attested = true);
}
}
]
}
}
}
},
}
},
columns : [
{ text : 'Employee no.', field : 'id', width : 100, align : 'center' },
{ text : 'Name', field : 'name', flex : 1 },
{ text : 'Start date', field : 'start', flex : 1, type : 'date' },
{
text : 'Email',
field : 'email',
flex : 1,
htmlEncode : false, // To be able to use HTML in the renderer
renderer : ({ value }) => StringHelper.xss`<i class="b-fa b-fa-envelope"></i><a href="mailto:${value}">${value}</a>`
}, {
text : 'Attested',
field : 'unattested',
width : 100,
editor : false,
renderer({ value, cellElement }) {
cellElement.style.color = value ? '#e53f2c' : '#4caf50';
return value ? value + ' left' : 'All done';
}
},
{ text : 'Total time', field : 'totalTime', width : 100, align : 'center', editor : false }
],
store : employeeStore,
listeners : {
renderRows : ({ source }) => {
// This will expand third row at a state where the theme has loaded
source.features.rowExpander.expand(source.store.getAt(2));
},
once : true
}
});
grid.on({
cellClick : ({ column, source, record }) => {
if (column.field === 'name') {
const widget = source.features.rowExpander.getExpandedWidgets(record);
const grid2 = widget.normal.widgetMap.grid2
grid2.hidden = !grid2.hidden;
console.log(grid2.hidden);
}
}
})
import { DataGenerator, RandomGenerator, DateHelper, Grid, GridRowModel, Store, StringHelper } from '../../build/grid.module.js?473556';
import shared from '../_shared/shared.module.js?473556';
const randomGenerator = new RandomGenerator();
export const getEmployees = () => {
const data = DataGenerator.generateData(25);
for (const row of data) {
DateHelper.add(row.start, row.id * 2, 'month', false);
row.email = `${row.name.toLowerCase().replaceAll(' ', '.')}@example.com`;
}
return data;
};
export const getTimeRows = (employees) => {
const
tasks = DataGenerator.generateEvents({ viewStartDate : new Date(), viewEndDate : new Date(), nbrResources : 1, nbrEvents : 35 }).events.map(e => e.name),
rows = [];
let id = 0;
for (const employee of employees) {
for (let i = 0; i < Math.ceil(Math.random() * 10); i++) {
id += 1;
rows.push({
id,
employeeId : employee.id,
name : randomGenerator.fromArray(tasks),
hours : Math.ceil(Math.random() * 100),
attested : Math.random() > 0.5
});
}
}
return rows;
};
// These two model classes and two stores uses the built-in data-relations feature to connect related records to each other.
class Employee extends GridRowModel {
static fields = ['id', 'firstName', 'name', 'start', 'email'];
// Example how to set up a "calculated" field
get unattested() {
return this.timeRows.reduce((acc, r) => acc + (r.attested ? 0 : 1), 0);
}
// Example how to set up a "calculated" field
get totalTime() {
return this.timeRows.reduce((acc, r) => acc + r.hours, 0);
}
}
class TimeRow extends GridRowModel {
static fields = ['id', 'employeeId', 'project', 'hours', 'attested'];
// The relations config, will set up the relationship between the two stores and model classes
static relations = {
employee : {
foreignKey : 'employeeId', // The id of the "other" record
foreignStore : 'employeeStore', // Must be set on the record's store
relatedCollectionName : 'timeRows' // The field in which these records will be accessible from the "other" record
}
};
}
const employeeStore = new Store({
modelClass : Employee,
data : getEmployees()
});
const timeRowStore = new Store({
modelClass : TimeRow,
employeeStore, // Must be set, see relations config above
data : getTimeRows(employeeStore.records)
});
const grid = new Grid({
appendTo : 'container',
features : {
rowExpander : {
// The widget config declares what type of Widget will be created on each expand
renderer({ record }) {
return {
type : 'container',
layout : 'vbox',
items : {
grid : {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
autoHeight : true, // Grid resizes to fit all rows
columns : [
{ text : 'Project', field : 'name', flex : 1 },
{ text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
{ text : 'Attested', field : 'attested', width : 100, type : 'check' }
],
bbar : [ // Bottom toolbar
// Add button which adds a row to the expanded grid and starts editing it
{
text : 'Add',
icon : 'b-icon-add',
onClick : ({ source }) => {
const
grid = source.up('grid'),
expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
[newRecord] = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });
grid.startEditing(newRecord);
}
}, '->', {
// Button that sets all rows as "attested"
text : 'Attest all',
icon : 'b-icon-check',
onClick : ({ source }) => {
const { store } = source.up('grid');
store.forEach(r => r.attested = true);
}
}
]
},
grid2 : {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
hidden : true,
autoHeight : true, // Grid resizes to fit all rows
columns : [
{ text : 'Project', field : 'name', flex : 1 },
{ text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
{ text : 'Attested', field : 'attested', width : 100, type : 'check' }
],
bbar : [ // Bottom toolbar
// Add button which adds a row to the expanded grid and starts editing it
{
text : 'Add',
icon : 'b-icon-add',
onClick : ({ source }) => {
const
grid = source.up('grid'),
expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
[newRecord] = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });
grid.startEditing(newRecord);
}
}, '->', {
// Button that sets all rows as "attested"
text : 'Attest all',
icon : 'b-icon-check',
onClick : ({ source }) => {
const { store } = source.up('grid');
store.forEach(r => r.attested = true);
}
}
]
}
}
}
},
}
},
columns : [
{ text : 'Employee no.', field : 'id', width : 100, align : 'center' },
{ text : 'Name', field : 'name', flex : 1 },
{ text : 'Start date', field : 'start', flex : 1, type : 'date' },
{
text : 'Email',
field : 'email',
flex : 1,
htmlEncode : false, // To be able to use HTML in the renderer
renderer : ({ value }) => StringHelper.xss`<i class="b-fa b-fa-envelope"></i><a href="mailto:${value}">${value}</a>`
}, {
text : 'Attested',
field : 'unattested',
width : 100,
editor : false,
renderer({ value, cellElement }) {
cellElement.style.color = value ? '#e53f2c' : '#4caf50';
return value ? value + ' left' : 'All done';
}
},
{ text : 'Total time', field : 'totalTime', width : 100, align : 'center', editor : false }
],
store : employeeStore,
listeners : {
renderRows : ({ source }) => {
// This will expand third row at a state where the theme has loaded
source.features.rowExpander.expand(source.store.getAt(2));
},
once : true
}
});
grid.on({
cellClick : ({ column, source, record }) => {
if (column.field === 'name') {
const widget = source.features.rowExpander.getExpandedWidgets(record);
const grid2 = widget.normal.widgetMap.grid2
grid2.hidden = !grid2.hidden;
console.log(grid2.hidden);
}
}
})
Thanks for your reply and the provided code. We appreciate your efforts in providing a detailed example but we would also like to explain to you our requirement that we need the expander to work on the button clicks, there would be two buttons, and the click event of each will work separately and expand the particular grid/container. Please see below code snippet and attached screenshot. Could you please help us with that? We have tried it with the same example link that you provided us. https://bryntum.com/products/grid/examples/nested-grid/
import { DataGenerator, RandomGenerator, DateHelper, Grid, GridRowModel, Store, StringHelper } from '../../build/grid.module.js?474079';
import shared from '../_shared/shared.module.js?474079';
const randomGenerator = new RandomGenerator();
export const getEmployees = () => {
const data = DataGenerator.generateData(25);
for (const row of data) {
DateHelper.add(row.start, row.id * 2, 'month', false);
row.email = `${row.name.toLowerCase().replaceAll(' ', '.')}@example.com`;
}
return data;
};
export const getTimeRows = (employees) => {
const
tasks = DataGenerator.generateEvents({ viewStartDate : new Date(), viewEndDate : new Date(), nbrResources : 1, nbrEvents : 35 }).events.map(e => e.name),
rows = [];
let id = 0;
for (const employee of employees) {
for (let i = 0; i < Math.ceil(Math.random() * 10); i++) {
id += 1;
rows.push({
id,
employeeId : employee.id,
name : randomGenerator.fromArray(tasks),
hours : Math.ceil(Math.random() * 100),
attested : Math.random() > 0.5
});
}
}
return rows;
};
// These two model classes and two stores uses the built-in data-relations feature to connect related records to each other.
class Employee extends GridRowModel {
static fields = ['id', 'firstName', 'name', 'start', 'email'];
// Example how to set up a "calculated" field
get unattested() {
return this.timeRows.reduce((acc, r) => acc + (r.attested ? 0 : 1), 0);
}
// Example how to set up a "calculated" field
get totalTime() {
return this.timeRows.reduce((acc, r) => acc + r.hours, 0);
}
}
class TimeRow extends GridRowModel {
static fields = ['id', 'employeeId', 'project', 'hours', 'attested'];
// The relations config, will set up the relationship between the two stores and model classes
static relations = {
employee : {
foreignKey : 'employeeId', // The id of the "other" record
foreignStore : 'employeeStore', // Must be set on the record's store
relatedCollectionName : 'timeRows' // The field in which these records will be accessible from the "other" record
}
};
}
const employeeStore = new Store({
modelClass : Employee,
data : getEmployees()
});
const timeRowStore = new Store({
modelClass : TimeRow,
employeeStore, // Must be set, see relations config above
data : getTimeRows(employeeStore.records)
});
const grid = new Grid({
appendTo : 'container',
features : {
rowExpander : {
column: { hidden: true },
// The widget config declares what type of Widget will be created on each expand
renderer({ record }) {
return {
type : 'container',
layout : 'vbox',
items : {
grid : {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
autoHeight : true, // Grid resizes to fit all rows
columns : [
{ text : 'Project', field : 'name', flex : 1 },
{ text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
{ text : 'Attested', field : 'attested', width : 100, type : 'check' }
],
bbar : [ // Bottom toolbar
// Add button which adds a row to the expanded grid and starts editing it
{
text : 'Add',
icon : 'b-icon-add',
onClick : ({ source }) => {
const
grid = source.up('grid'),
expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
[newRecord] = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });
grid.startEditing(newRecord);
}
}, '->', {
// Button that sets all rows as "attested"
text : 'Attest all',
icon : 'b-icon-check',
onClick : ({ source }) => {
const { store } = source.up('grid');
store.forEach(r => r.attested = true);
}
}
]
},
grid2 : {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
hidden : true,
autoHeight : true, // Grid resizes to fit all rows
columns : [
{ text : 'Project', field : 'name', flex : 1 },
{ text : 'Hours spent', field : 'hours', width : 100, type : 'number' },
{ text : 'Attested', field : 'attested', width : 100, type : 'check' }
],
bbar : [ // Bottom toolbar
// Add button which adds a row to the expanded grid and starts editing it
{
text : 'Add',
icon : 'b-icon-add',
onClick : ({ source }) => {
const
grid = source.up('grid'),
expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
[newRecord] = grid.store.add({ name : null, hours : 0, employeeId : expandedRecord.id });
grid.startEditing(newRecord);
}
}, '->', {
// Button that sets all rows as "attested"
text : 'Attest all',
icon : 'b-icon-check',
onClick : ({ source }) => {
const { store } = source.up('grid');
store.forEach(r => r.attested = true);
}
}
]
}
}
}
},
}
},
columns : [
{ text : 'Employee no.', field : 'id', width : 100, align : 'center' },
{ text : 'Name', field : 'name', flex : 1 },
{ text : 'Total time', field : 'totalTime', width : 100, align : 'center', editor : false },
{
field: "show_details",
text: "",
width: 200,
default: true,
action: true,
editor: false,
type: "widget",
align: "center",
widgets: [
{
type: "button",
cls: "b-raised",
color: "b-green",
text: "",
onClick: ({ source: btn }) => {
const { record } = btn.cellInfo;
this.toggleDetails(record.data, record.parentIndex, "1");
},
},
],
renderer: ({ record, widgets }) => {
widgets[0].text = Boolean(record._toggled) ? "Hide Details" : "Show Details"; if (
parseInt(record.Matched) === 3 ||
parseInt(record.Posted) === 1
) {
widgets[0].disabled = true;
} else {
widgets[0].disabled = false;
}
},
},
{
field: "show_detailsnote",
text: "",
width: 200,
default: true,
action: true,
editor: false,
type: "widget",
align: "center",
widgets: [
{
type: "button",
cls: "b-raised",
color: "b-green",
text: "",
onClick: ({ source: btn }) => {
const { record } = btn.cellInfo;
console.log(record);
//this.toggleDetailsnote(record, record.parentIndex);
// console.log(this.gridConfig.features.rowExpander);
// const rowExpander = this.gridConfig.features.rowExpander; // if (rowExpander) {
// rowExpander.expand(record);
// }
// this.gridConfig.features.rowExpander.expand(record);
},
},
],
renderer: ({ record, widgets }) => {
console.log(record._togglednote);
widgets[0].text = Boolean(record._togglednote)
? "Hide Note"
: "Show Note";
},
},
],
store : employeeStore,
listeners : {
renderRows : ({ source }) => {
// This will expand third row at a state where the theme has loaded
source.features.rowExpander.expand(source.store.getAt(2));
},
once : true
}
});
grid.on({
cellClick : ({ column, source, record }) => {
if (column.field === 'name') {
const widget = source.features.rowExpander.getExpandedWidgets(record);
const grid2 = widget.normal.widgetMap.grid2
grid2.hidden = !grid2.hidden;
console.log(grid2.hidden);
}
}
});
I built something half-working out of your example code. Here it is:
const randomGenerator = new RandomGenerator();
export const getEmployees = () => {
const data = DataGenerator.generateData(25);
for (const row of data) {
DateHelper.add(row.start, row.id * 2, 'month', false);
row.email = `${row.name.toLowerCase().replaceAll(' ', '.')}@example.com`;
}
return data;
};
export const getTimeRows = (employees) => {
const
tasks = DataGenerator.generateEvents({ viewStartDate : new Date(), viewEndDate : new Date(), nbrResources : 1, nbrEvents : 35 }).events.map(e => e.name),
rows = [];
let id = 0;
for (const employee of employees) {
for (let i = 0; i < Math.ceil(Math.random() * 10); i++) {
id += 1;
rows.push({
id,
employeeId : employee.id,
name : randomGenerator.fromArray(tasks),
hours : Math.ceil(Math.random() * 100),
attested : Math.random() > 0.5
});
}
}
return rows;
};
// These two model classes and two stores uses the built-in data-relations feature to connect related records to each other.
class Employee extends GridRowModel {
static fields = ['id', 'firstName', 'name', 'start', 'email'];
// Example how to set up a "calculated" field
get unattested() {
return this.timeRows.reduce((acc, r) => acc + (r.attested ? 0 : 1), 0);
}
// Example how to set up a "calculated" field
get totalTime() {
return this.timeRows.reduce((acc, r) => acc + r.hours, 0);
}
}
class TimeRow extends GridRowModel {
static fields = ['id', 'employeeId', 'project', 'hours', 'attested'];
// The relations config, will set up the relationship between the two stores and model classes
static relations = {
employee : {
foreignKey : 'employeeId', // The id of the "other" record
foreignStore : 'employeeStore', // Must be set on the record's store
relatedCollectionName : 'timeRows' // The field in which these records will be accessible from the "other" record
}
};
}
const employeeStore = new Store({
modelClass : Employee,
data : getEmployees()
});
const timeRowStore = new Store({
modelClass : TimeRow,
employeeStore, // Must be set, see relations config above
data : getTimeRows(employeeStore.records)
});
const grid = new Grid({
appendTo : 'container',
features : {
rowExpander : {
column : { hidden : true },
enableAnimations : true,
// The widget config declares what type of Widget will be created on each expand
renderer({ record }) {
const { _show } = record;
record._show = null;
if (_show === 'time') {
return {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
autoHeight : true, // Grid resizes to fit all rows
dataField : 'timeRows',
columns : [
{
text : 'Project',
field : 'name',
flex : 1
},
{
text : 'Hours spent',
field : 'hours',
width : 100,
type : 'number'
},
{
text : 'Attested',
field : 'attested',
width : 100,
type : 'check'
}
],
bbar : [ // Bottom toolbar
// Add button which adds a row to the expanded grid and starts editing it
{
text : 'Add',
icon : 'b-icon-add',
onClick : ({ source }) => {
const
grid = source.up('grid'),
expandedRecord = grid.owner.features.rowExpander.getExpandedRecord(grid),
[newRecord] = grid.store.add({
name : null,
hours : 0,
employeeId : expandedRecord.id
});
grid.startEditing(newRecord);
}
}, '->', {
// Button that sets all rows as "attested"
text : 'Attest all',
icon : 'b-icon-check',
onClick : ({ source }) => {
const { store } = source.up('grid');
store.forEach(r => r.attested = true);
}
}
]
};
}
return {
cls : 'timerow-grid', // CSS class added to the outer element
type : 'grid',
autoHeight : true, // Grid resizes to fit all rows
columns : [
{
text : 'Notes',
field : 'name',
flex : 1
}
]
};
}
}
},
columns : [
{ text : 'Employee no.', field : 'id', width : 100, align : 'center' },
{ text : 'Name', field : 'name', flex : 1 },
{ text : 'Total time', field : 'totalTime', width : 100, align : 'center', editor : false },
{
field : 'show_details',
text : '',
width : 200,
default : true,
action : true,
editor : false,
type : 'widget',
align : 'center',
widgets : [
{
type : 'button',
cls : 'b-raised',
color : 'b-green',
text : '',
onClick : ({ source: btn }) => {
const
{ record } = btn.cellInfo,
{ rowExpander } = btn.closest('grid').features;
record._show = 'time';
rowExpander.collapse(record).then(() => {
rowExpander.expand(record);
});
}
}
],
renderer : ({ record, widgets }) => {
widgets[0].text = Boolean(record._toggled) ? 'Hide Details' : 'Show Details';
if (
parseInt(record.Matched) === 3 ||
parseInt(record.Posted) === 1
) {
widgets[0].disabled = true;
}
else {
widgets[0].disabled = false;
}
}
},
{
field : 'show_detailsnote',
text : '',
width : 200,
default : true,
action : true,
editor : false,
type : 'widget',
align : 'center',
widgets : [
{
type : 'button',
cls : 'b-raised',
color : 'b-green',
text : '',
onClick : ({ source: btn }) => {
const
{ record } = btn.cellInfo,
{ rowExpander } = btn.closest('grid').features;
rowExpander.collapse(record).then(() => {
rowExpander.expand(record);
});
}
}
],
renderer : ({ record, widgets }) => {
console.log(record._togglednote);
widgets[0].text = Boolean(record._togglednote)
? 'Hide Note'
: 'Show Note';
}
}
],
store : employeeStore
});
Some highlights:
I'm recreating the expanded grid on each button press. This way you can use the built in widget functionality and map the expanded grid's store to the "outer" row's record's dataField.
The rowExpander renderer only returns one grid, not both, for same reason as above.
I've disabled animations on the rowExpander feature because it looks bad with this code if you "change" an already expanded record. This could be experimented with.
There is another way of doing this as well, and that is the way tasnim suggested. That way requires you to set the Store and data of each expanded grid.
So, if you're using Model/Store relations and can "connect" the expanded grid to a field on the expanded record, I think the example I gave you would work best.