RowExpander
Enables expanding of Grid rows by either row click or double click, or by adding a separate Grid column which renders a button that expands or collapses the row.
//<code-header>
fiddle.title = 'Row expander';
//</code-header>
const grid = new Grid({
appendTo : targetElement,
height : 320,
features : {
// Enable the feature
rowExpander : {
columnPosition : 'last',
renderer({ record }) {
return `<div style="padding: 10px"><div style="font-weight: bold;margin-bottom:5px;">Introduction in Latin</div><div style="color:#555">${record.notes} ${record.notes}</div></div>`;
}
}
},
data : DataGenerator.generateData(15),
columns : [
{ field : 'firstName', text : 'First name', flex : 1 },
{ field : 'surName', text : 'Surname', flex : 1 },
{ field : 'age', text : 'Age', flex : 1 }
]
});
grid.features.rowExpander.expand(grid.store.first);The content of the expanded row body is rendered by providing either a renderer function to the rowExpander feature config:
new Grid({
features : {
rowExpander : {
renderer({record, region, expanderElement}){
return htmlToBeExpanded;
}
}
}
});
Or a widget configuration object:
new Grid({
features : {
rowExpander : {
widget : {
type : 'detailGrid',
},
dataField : 'orderDetails'
}
}
});
//<code-header>
fiddle.title = 'Row expander widget';
//</code-header>
class LineItem extends GridRowModel {
static fields = [
'name',
'quantity',
{ name : 'price', type : 'number' }
];
// Computed field that sums up the lines total price
get total() {
return this.price * this.quantity;
}
}
class Order extends GridRowModel {
static fields = [
{ name : 'date', type : 'date' },
// This is the field from which the expanded LineItemGrid will get its data
// The type "store" means that this field has a number of records in itself
{ name : 'details', type : 'store', storeClass : Store, modelClass : LineItem }
];
// Computed field that sums up the order total
get total() {
return this.details?.sum('total') || 0;
}
}
const detailsGridConfig = {
type : 'grid',
// The Grid will adjust its height to fit all rows
autoHeight : true,
selectionMode : {
// Adds a checkbox column that lets the user select rows
checkbox : true,
// Adds a checkbox to the checkbox column header that lets the user check/uncheck all rows
showCheckAll : true
},
columns : [
{
// A template column that renders an icon next to the product name
type : 'template',
text : 'Product',
field : 'name',
flex : 3,
template : ({ record }) => `<i class="${record.icon} fa-fw" style="margin-right:.5em"></i>${record.name}`
},
{
// A widget column that lets the user increase and decrease the quantity value
type : 'number',
text : 'Quantity',
field : 'quantity',
width : 150
},
{
text : 'Price',
type : 'number',
field : 'price',
width : 100,
format : {
style : 'currency',
currency : 'USD'
}
},
{
type : 'action',
width : 40,
actions : [{
cls : 'fa fa-trash',
tooltip : 'Delete item',
onClick : async({ record }) => {
if (await MessageDialog.confirm({
title : 'Please confirm',
message : 'Delete this line item?'
}) === MessageDialog.yesButton) {
record.remove();
}
}
}]
}
]
};
const grid = new Grid({
appendTo : targetElement,
height : 400,
store : {
modelClass : Order
},
features : {
// Enable the feature
rowExpander : {
dataField : 'details',
widget : detailsGridConfig
}
},
columns : [
{ field : 'id', text : 'Order', flex : 1 },
{ field : 'date', text : 'Date', type : 'date', flex : 1 },
{
type : 'number',
field : 'total',
text : 'Total',
align : 'right',
width : 100,
format : {
style : 'currency',
currency : 'USD'
}
}
],
data : [
{
id : '123456',
date : '2022-12-10',
details : [
{ id : 111, name : 'Milk', icon : 'fa fa-cow', quantity : 2, price : 1.99 },
{ id : 112, name : 'Bread', icon : 'fa fa-bread-slice', quantity : 1, price : 2.49 },
{ id : 113, name : 'Eggs', icon : 'fa fa-egg', quantity : 4, price : 0.99 },
{ id : 114, name : 'Apples', icon : 'fa fa-apple-whole', quantity : 3, price : 0.69 }
]
},
{
id : '4368943',
date : '2022-12-22',
details : [
{ id : 122, name : 'Rice', icon : 'fa fa-bowl-rice', quantity : 2, price : 3.49 },
{ id : 124, name : 'Lemons', icon : 'fa fa-lemon', quantity : 1, price : 0.69 },
{ id : 121, name : 'Peppers', icon : 'fa fa-pepper-hot', quantity : 4, price : 2.99 },
{ id : 123, name : 'Cookies', icon : 'fa fa-cookie-bite', quantity : 3, price : 1.99 }
]
},
{
id : '789012',
date : '2022-12-12',
details : [
{ id : 211, name : 'Chicken', icon : 'fa fa-drumstick-bite', quantity : 2, price : 4.99 },
{ id : 212, name : 'Carrots', icon : 'fa fa-carrot', quantity : 1, price : 1.49 },
{ id : 213, name : 'Wine', icon : 'fa fa-wine-bottle', quantity : 4, price : 2.99 },
{ id : 214, name : 'Cheese', icon : 'fa fa-cheese', quantity : 3, price : 3.49 },
{ id : 215, name : 'Bottled Water', icon : 'fa fa-bottle-water', quantity : 5, price : 0.99 }
]
}
]
});
grid.features.rowExpander.expand(grid.store.first);This feature is disabled by default
Expand on click
Set triggerEvent to a Grid cell event that should trigger row expanding and collapsing.
new Grid({
features : {
rowExpander : {
triggerEvent: 'celldblclick',
renderer...
}
}
});
Expander column position
The expander column can either be inserted before or after the existing Grid columns. If the Grid has multiple regions the column will be added to the first region.
Adjust expander column position to last in a specific Grid region by setting columnPosition
to last and configuring the column with a region name.
new Grid({
features : {
rowExpander : {
column: {
region: 'last'
},
columnPosition: 'last',
renderer...
}
}
});
Record update
If the expander content depends on row record data, the expander can be re-rendered on record update by setting
refreshOnRecordChange to true.
new Grid({
features : {
rowExpander : {
refreshOnRecordChange: true,
renderer...
}
}
});
Async
When the content of the row expander should be rendered async just see to it that you return a promise.
new Grid({
features : {
rowExpander : {
async renderer({record, region, expanderElement}){
return fetchFromBackendAndRenderData(record);
}
}
}
});
Multiple regions
When the Grid has more than one region, the renderer function will be called once per region for each expanding row.
new Grid({
features : {
rowExpander : {
renderer({ record, region }) {
if(region === 'locked') {
return createRowExpander(record);
}
return null;
}
}
}
});
If you are using the widget configuration, you can provide a widget configuration object for each region like so:
new Grid({
features : {
rowExpander : {
widget : {
locked : {
type : 'detailGrid',
// If your widgets uses different data sources, put the
// dataField property in the widget configuration object
dataField : 'orderDetails'
},
normal : {
type : 'summaryGrid',
dataField : 'sumDetails'
}
}
}
}
});
This live demo has a set of buttons in the locked region and a detail grid in the normal region:
//<code-header>
fiddle.title = 'Row expander widgets';
//</code-header>
const grid = new Grid({
appendTo : targetElement,
height : 400,
features : {
// Enable the feature
rowExpander : {
widget : {
locked : {
type : 'panel',
title : 'Actions',
items : {
addSkill : { type : 'button', text : 'Add skill', width : '100%' },
openReport : { type : 'button', text : 'Open time report', width : '100%' },
deactivate : { type : 'button', text : 'Deactivate', width : '100%' }
}
},
normal : {
type : 'grid',
dataField : 'skills',
autoHeight : true,
columns : [
{ field : 'id', text : 'No.', type : 'number', width : 80 },
{ field : 'skill', text : 'Skill', flex : 1 },
{ field : 'level', text : 'Level', type : 'number', width : 100 },
{ field : 'verified', text : 'Verified', type : 'check', width : 100 }
]
}
}
}
},
subGridConfigs : {
locked : {
width : 190
},
normal : {
flex : 1
}
},
columns : [
{ field : 'name', text : 'Name', width : 150, region : 'locked' },
{ field : 'id', text : 'Employee no.', type : 'number', width : 110, region : 'normal', align : 'center' },
{ field : 'city', text : 'City', width : 110, region : 'normal' },
{ field : 'age', text : 'Age', type : 'number', width : 90, region : 'normal', align : 'center' },
{ field : 'start', text : 'Start', type : 'date', width : 110, region : 'normal' },
{
field : 'name',
text : 'Email',
width : 220,
region : 'normal',
htmlEncode : false,
renderer : ({ value }) => {
const email = value.toLowerCase().replaceAll(' ', '.') + '@example.com';
return StringHelper.xss`<i class="fa fa-envelope"></i><a href="mailto:${email}">${email}</a>`;
}
},
{ field : 'active', text : 'Active', type : 'check', width : 90, region : 'normal' }
],
data : DataGenerator.generateData({
count : 10,
addSkills : true,
rowCallback(row) {
row.skills = row.skills.map((skill, index) => ({ id : index + 1, skill, level : Math.round(Math.random() * 2) + 1, verified : Math.random() > 0.5 }));
}
})
});
// How to programmatically expand a specific row:
//grid.features.rowExpander.expand(grid.store.first);If you want your expanded content to span over all Grid regions, set the spanRegions config to
true.
//<code-header>
fiddle.title = 'Row expander span regions';
//</code-header>
const grid = new Grid({
appendTo : targetElement,
height : 400,
features : {
// Enable the feature
rowExpander : {
spanRegions : true,
widget : {
type : 'grid',
dataField : 'skills',
autoHeight : true,
columns : [
{ field : 'id', text : 'No.', type : 'number', width : 80 },
{ field : 'skill', text : 'Skill', flex : 1 },
{ field : 'level', text : 'Level', type : 'number', width : 100 },
{ field : 'verified', text : 'Verified', type : 'check', width : 100 }
]
}
}
},
subGridConfigs : {
locked : {
width : 190
},
normal : {
flex : 1
}
},
columns : [
{ field : 'name', text : 'Name', width : 150, region : 'locked' },
{ field : 'id', text : 'Employee no.', type : 'number', width : 110, region : 'normal', align : 'center' },
{ field : 'city', text : 'City', width : 110, region : 'normal' },
{ field : 'age', text : 'Age', type : 'number', width : 90, region : 'normal', align : 'center' },
{ field : 'start', text : 'Start', type : 'date', width : 110, region : 'normal' },
{
field : 'name',
text : 'Email',
width : 220,
region : 'normal',
htmlEncode : false,
renderer : ({ value }) => {
const email = value.toLowerCase().replaceAll(' ', '.') + '@example.com';
return StringHelper.xss`<i class="fa fa-envelope"></i><a href="mailto:${email}">${email}</a>`;
}
},
{ field : 'active', text : 'Active', type : 'check', width : 90, region : 'normal' }
],
data : DataGenerator.generateData({
count : 10,
addSkills : true,
rowCallback(row) {
row.skills = row.skills.map((skill, index) => ({ id : index + 1, skill, level : Math.round(Math.random() * 2) + 1, verified : Math.random() > 0.5 }));
}
})
});Configs
21
Configs
21Other
When expanding a row and the expanded body element is not completely in view, setting this to true will
automatically scroll the expanded row into view.
Provide a column config object to display a button with expand/collapse functionality.
Shown by default, set to null to not include.
new Grid({
features : {
rowExpander : {
column: {
// Use column config options here
region: 'last'
}
}
}
});
Makes the expand/collapse button column appear either as the first column (default or first) or as the
last (set to last). Note that the column by default will be added to the first region, if the Grid
has multiple regions. Use the column config to change region.
Used together with widget to populate the widget's Store from the expanded record's
corresponding dataField value, which needs to be an array of objects or a store itself.
Use this to disable expand and collapse animations.
Use this for customizing async renderer loading indicator height.
Use this for customizing async renderer loading indicator text.
The name of the Grid event that will toggle expander. Defaults to null but can be set to any event such
as cellDblClick or
cellClick.
features : {
rowExpander : {
triggerEvent : 'cellclick'
}
}
A widget configuration object that will be used to create a widget to render into the row expander body. Can be used instead of providing a renderer.
If the widget needs a store, it can be populated by use of the dataField config. This will
create a store from the expanded record's corresponding dataField value, which needs to be an array of
objects or a store itself.
new Grid({
features : {
rowExpander : {
widget : {
type : 'detailGrid',
},
dataField : 'orderDetails'
}
}
});
If there is multiple regions, you can configure each region like so:
new Grid({
features : {
rowExpander : {
widget : {
// The region name is the property, and its widget config the value
left : {
type : 'detailGrid',
// If your widgets uses different data sources, put the dataField
// property in the widget configuration object
dataField : 'orderDetails'
},
middle : {
type : 'summaryGrid',
dataField : 'sumDetails
},
// No expander here
right : null
}
}
}
})
Rendering
The implementation of this function is called each time the body of an expanded row is rendered. Either return an HTML string, a DomConfig object describing the markup or any Widget configuration object, like a Grid configuration object for example.
new Grid({
features : {
rowExpander : {
renderer({record, region, expanderElement}){
return htmlToBeExpanded;
}
}
}
});
Or return a DomConfig object.
new Grid({
features : {
rowExpander : {
renderer({record, region, expanderElement}){
return {
tag : 'form',
className : 'expanded-row-form',
children : [
{
tag : 'textarea',
name : 'description',
className : 'expanded-textarea'
},
{
tag : 'button',
text : 'Save',
className : 'expanded-save-button',
}
]
};
}
}
}
});
Or return a Widget configuration object. What differs a Widget configuration object from a DomConfig object
is the presence of the type property and the absence of a tag property.
new Grid({
features : {
rowExpander : {
async renderer({record, region, expanderElement}){
const myData = await fetch('myURL');
return {
type : 'grid',
autoHeight : true,
columns : [
...
],
data : myData
};
}
}
}
});
It is also possible to add markup directly to the expanderElement.
new Grid({
features : {
rowExpander : {
renderer({record, region, expanderElement}){
new UIComponent({
appendTo: expanderElement,
...
});
}
}
}
});
The renderer function can also be asynchronous.
new Grid({
features : {
rowExpander : {
async renderer({record, region, expanderElement}){
return await awaitAsynchronousOperation();
}
}
}
});
| Parameter | Type | Description |
|---|---|---|
renderData | Object | Object containing renderer parameters |
renderData.record | Model | Record for the row |
renderData.expanderElement | HTMLElement | Expander body element |
renderData.rowElement | HTMLElement | Row element |
renderData.region | String | Grid region name |
renderData.grid | Grid | Grid instance |
Row expander body content
Misc
Properties
15
Properties
15Common
Class hierarchy
Other
Functions
35
Functions
35Common
Tells the RowExpander that the provided record should be collapsed. If the record is in view, it will be collapsed. If the record is not in view, it will simply not be expanded when rendered into view.
| Parameter | Type | Description |
|---|---|---|
record | Model | Record whose row should be collapsed |
Tells the RowExpander that the provided record should be expanded. If or when the record is rendered into view, the record will be expanded.
Promise will resolve when the row gets expanded. Note that this can be much later than the actual expand call, depending on response times and if current record is in view or not.
| Parameter | Type | Description |
|---|---|---|
record | Model | Record whose row should be expanded |
Tells the RowExpander that the provided expanded record content should be refreshed. If or when the record is rendered into view, the content will be refreshed.
Promise will resolve when the grid gets refreshed. Note that this does not mean that the provided record content has been re-rendered yet, as it could be scrolled out of view.
| Parameter | Type | Description |
|---|---|---|
record | Model | Record whose row should be refreshed |
Other
Gets the corresponding expanded record from either a nested widget or an element in the expanded body.
| Parameter | Type | Description |
|---|---|---|
elementOrWidget | HTMLElement | Widget |
Gets the expanded widget(s) for a specified record. The widget(s) will be returned as an object with region names as properties and the widgets as values.
| Parameter | Type | Description |
|---|---|---|
record | Model |
Rows
Tells the RowExpander that the provided record should be collapsed. If the record is in view, it will be collapsed. If the record is not in view, it will simply not be expanded when rendered into view.
| Parameter | Type | Description |
|---|---|---|
record | Model | Record whose row should be collapsed |
Tells the RowExpander that the provided record should be expanded. If or when the record is rendered into view, the record will be expanded.
Promise will resolve when the row gets expanded. Note that this can be much later than the actual expand call, depending on response times and if current record is in view or not.
| Parameter | Type | Description |
|---|---|---|
record | Model | Record whose row should be expanded |
Configuration
Events
Misc
Events
9
Events
9This event fires before row collapse is started.
Returning false from a listener prevents the RowExpander to collapse the row.
Note that this event fires when the RowExpander toggles the row, not when the actual row expander body is rendered. Most of the time this is synchronous, but in the case of a row that is not yet rendered into view by scrolling, it can happen much later.
// Adding a listener using the "on" method
rowExpander.on('beforeRowCollapse', ({ record }) => {
});| Parameter | Type | Description |
|---|---|---|
record | Model | Record |
This event fires before row expand is started.
Returning false from a listener prevents the RowExpander to expand the row.
Note that this event fires when the RowExpander toggles the row, not when the actual row expander body is rendered. Most of the time this is synchronous, but in the case of a row that is not yet rendered into view by scrolling, it can happen much later.
// Adding a listener using the "on" method
rowExpander.on('beforeRowExpand', ({ record }) => {
});| Parameter | Type | Description |
|---|---|---|
record | Model | Record |
This event fires when a row has finished collapsing.
// Adding a listener using the "on" method
rowExpander.on('rowCollapse', ({ record }) => {
});| Parameter | Type | Description |
|---|---|---|
record | Model | Record |
This event fires when a row expand has finished expanding.
Note that this event fires when actual row expander body is rendered, and not necessarily in immediate succession of an expand action. In the case of expanding a row that is not yet rendered into view by scrolling, it can happen much later.
// Adding a listener using the "on" method
rowExpander.on('rowExpand', ({ record, expandedElements, widget, widgets }) => {
});| Parameter | Type | Description |
|---|---|---|
record | Model | Record |
expandedElements | Object | An object with the Grid region name as property and the expanded body element as value |
widget | Widget | In case of expanding a Widget, this will be a reference to the instance
created by the actual expansion. If there is multiple Grid regions, use the |
widgets | Object | In case of expanding a Widget, this will be an object with the Grid region name as property and the reference to the widget instance created by the actual expansion |
Event handlers
9
Event handlers
9This event called before row collapse is started.
Returning false from a listener prevents the RowExpander to collapse the row.
Note that this event called when the RowExpander toggles the row, not when the actual row expander body is rendered. Most of the time this is synchronous, but in the case of a row that is not yet rendered into view by scrolling, it can happen much later.
new RowExpander({
onBeforeRowCollapse({ record }) {
}
});| Parameter | Type | Description |
|---|---|---|
record | Model | Record |
This event called before row expand is started.
Returning false from a listener prevents the RowExpander to expand the row.
Note that this event called when the RowExpander toggles the row, not when the actual row expander body is rendered. Most of the time this is synchronous, but in the case of a row that is not yet rendered into view by scrolling, it can happen much later.
new RowExpander({
onBeforeRowExpand({ record }) {
}
});| Parameter | Type | Description |
|---|---|---|
record | Model | Record |
This event called when a row has finished collapsing.
new RowExpander({
onRowCollapse({ record }) {
}
});| Parameter | Type | Description |
|---|---|---|
record | Model | Record |
This event called when a row expand has finished expanding.
Note that this event called when actual row expander body is rendered, and not necessarily in immediate succession of an expand action. In the case of expanding a row that is not yet rendered into view by scrolling, it can happen much later.
new RowExpander({
onRowExpand({ record, expandedElements, widget, widgets }) {
}
});| Parameter | Type | Description |
|---|---|---|
record | Model | Record |
expandedElements | Object | An object with the Grid region name as property and the expanded body element as value |
widget | Widget | In case of expanding a Widget, this will be a reference to the instance
created by the actual expansion. If there is multiple Grid regions, use the |
widgets | Object | In case of expanding a Widget, this will be an object with the Grid region name as property and the reference to the widget instance created by the actual expansion |
Typedefs
1
Typedefs
1CSS variables
6
CSS variables
6| Name | Description |
|---|---|
--b-row-expander-border-bottom-width | Row expander body's border bottom width |
--b-row-expander-font-weight | Row expander body's font weight |
--b-row-expander-background | Row expander body's background color |
--b-row-expander-padding | Row expander body's padding |
--b-row-expander-border-bottom-color | Row expander body's border bottom color |
--b-row-expander-color | Row expander body's color |