RowExpander
Feature
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.
const grid = new Grid({ appendTo : targetElement, height : 320, features : { // Enable the feature rowExpander : { columnPosition : 'last', renderer({ record }) { return `<div><div>Introduction in Latin</div><div>${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'
}
}
});
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"></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); fixedRowHeight must be set to false.This feature is disabled by default
Keyboard shortcuts
This feature has the following default keyboard shortcuts:
| Keys | Action | Action description |
|---|---|---|
Shift+ArrowRight |
expandByKey | Expands the focused row |
Shift+ArrowLeft |
collapseByKey | Collapses the focused row |
Space |
toggleExpandByKey | Toggles expand/collapse when focused on the expander cell |
Ctrl is the equivalent to Command and Alt is the equivalent to Option for Mac users For more information on how to customize keyboard shortcuts, please see our guide
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:
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-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.
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-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 })); } }) }); See also
- Grid - The grid component
Configs
Configs are options you supply in a configuration object when creating an instance of this class-
When expanding a row and the expanded body element is not completely in view, setting this to
truewill automatically scroll the expanded row into view. -
Makes the expand/collapse button column appear either as the first column (default or
first) or as the last (set tolast). 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
dataFieldvalue, which needs to be an array of objects or a store itself. -
Use this to disable expand and collapse animations.
-
keyMap : Object<String, KeyMapConfig>
See Keyboard shortcuts for details
-
Use this for customizing async renderer loading indicator height.
-
Use this for customizing async renderer loading indicator text.
-
Internal listeners, that cannot be removed by the user.
-
The widget which this plugin is to attach to.
Has a corresponding runtime client property.
-
Set to
falseto disable localization of this object.
Properties
Properties are getters/setters or publicly accessible variables on this class-
Identifies an object as an instance of Events class, or subclass thereof.
-
Identifies an object as an instance of Localizable class, or subclass thereof.
-
Identifies an object as an instance of RowExpander class, or subclass thereof.
-
A class property getter for the default values of internal properties for this class.
-
An empty array that can be used as a default value.
-
An empty object that can be used as a default value.
-
Identifies an object as an instance of InstancePlugin class, or subclass thereof.
-
Identifies an object as an instance of RowExpander class, or subclass thereof.
-
Returns a copy of the full configuration which was used to configure this object.
-
This property is set to
truebefore theconstructorreturns. -
This property is set to
trueon entry to the destroy method. It remains on the objects after returning fromdestroy(). If isDestroyed istrue, this property will also betrue, so there is no need to test for both (for example,comp.isDestroying || comp.isDestroyed). -
The Widget which was passed into the constructor, which is the Widget we are providing extra services for.
Has a corresponding client config.
-
Get the global LocaleHelper
-
Get the global LocaleManager
Functions
Functions are methods available for calling on the class-
This optional class method is called when a class is mixed in using the mixin() method.
-
Registers this class type with its Factory
-
collapseByKey( )private
Collapses the focused row on
Shift+ArrowLeft. -
expandByKey( )private
Expands the focused row on
Shift+ArrowRight. -
toggleExpandByKey( )private
Toggles expand/collapse when focused on the expander column cell. Only activates when the focus is on the expander column to avoid conflicts with other Space handlers.
-
Internal function used to hook destroy() calls when using thisObj
-
Internal function used restore hooked destroy() calls when using thisObj
-
Auto detaches listeners registered from start, if set as detachable
-
Internal function used to run a callback function after an event is triggered
-
Removes all listeners registered to this object by the application.
-
This will merge a feature's (subclass of InstancePlugin) keyMap with it's client's keyMap.
-
beforeRenderRow( )private
Hooks on before row render to render or remove row expander content depending on record state.
-
internalRender( )private
Re-renders the grid from the topmost record of those saved in bufferedRenderer
-
scrollRowIntoView( )private
Scrolls expanded row into view. This function is called after rowManager has finished rendering.
-
waitForTransition( )private
Waits for height transition on the provided rows element. Then calls provided function.
Events
Events are triggered for certain actions in this class and can be listened for to react to those actions in your codeEvent handlers
Event handlers are callbacks called as a result of certain actions in this classCSS variables
CSS variables that can be set to adjust appearance| Name | Description |
|---|---|
| --b-row-expander-background | Row expander body's background color |
| --b-row-expander-border-bottom-color | Row expander body's border bottom color |
| --b-row-expander-border-bottom-width | Row expander body's border bottom width |
| --b-row-expander-color | Row expander body's color |
| --b-row-expander-font-weight | Row expander body's font weight |
| --b-row-expander-padding | Row expander body's padding |