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.

Row expander
//<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'
       }
   }
});

Row expander widget
//<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);

Note that if used in a Gantt, the Gantt's `fixedRowHeight` must be set to `false`.

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:

Row expander widgets
//<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.

Row expander span regions
//<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

Common

disabledInstancePlugin
listenersEvents

Other

autoScroll: Boolean= false

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'
           }
       }
   }
});
columnPosition: first | last= first

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.

dataField: String

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.

enableAnimations: Boolean= true

Use this to disable expand and collapse animations.

Use this for customizing async renderer loading indicator height.

loadingIndicatorText: String= Loading

Use this for customizing async renderer loading indicator text.

refreshOnRecordChange: Boolean= false

If set to true, the RowExpander will, on record update, re-render an expanded row by calling the renderer function or recreate the configured widget.

spanRegions: Boolean= false

When the Grid has multiple regions, setting this config to true changes how the expanded content is created and rendered. Instead of calling renderer once per region (or one widget per region) it will only create one expanded element which will span the full grid width regardless of Grid regions.

triggerEvent: String

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
            }
        }
    }
})
Please note that usage of this config requires access to the Shadow DOM. If your app lives in a sandboxed environment with restrictions on Shadow DOM manipulation, this is not supported. Known example of such environment is the SalesForce Lightning Web Components running without Lightning Web Security enabled.

Rendering

renderer: function

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.

You should never modify any records inside this method.
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();
           }
       }
   }
});
Please note that returning a Widget configuration object requires access to the Shadow DOM. If your app lives in a sandboxed environment with restrictions on Shadow DOM manipulation, this is not supported. Known example of such environment is the SalesForce Lightning Web Components running without Lightning Web Security enabled.
ParameterTypeDescription
renderDataObject

Object containing renderer parameters

renderData.recordModel

Record for the row

renderData.expanderElementHTMLElement

Expander body element

renderData.rowElementHTMLElement

Row element

renderData.regionString

Grid region name

renderData.gridGrid

Grid instance

Returns: String | DomConfig | null -

Row expander body content

Misc

clientInstancePlugin
localeClassLocalizable
localizableLocalizable

Properties

15

Common

disabledInstancePlugin

Class hierarchy

isRowExpander: Boolean= truereadonly
Identifies an object as an instance of RowExpander class, or subclass thereof.
isRowExpander: Boolean= truereadonlystatic
Identifies an object as an instance of RowExpander class, or subclass thereof.
isEventsEvents
isInstancePluginInstancePlugin
isLocalizableLocalizable

Lifecycle

configBase

Misc

clientInstancePlugin
localeHelperLocalizable
localeManagerLocalizable

Other

Functions

35

Common

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.

ParameterTypeDescription
recordModel

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.

ParameterTypeDescription
recordModel

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.

ParameterTypeDescription
recordModel

Record whose row should be refreshed

Other

Gets the corresponding expanded record from either a nested widget or an element in the expanded body.

ParameterTypeDescription
elementOrWidgetHTMLElement | Widget
Returns: Model

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.

ParameterTypeDescription
recordModel
Returns: Widget
LstaticLocalizable
onEvents
relayAllEvents
triggerEvents
unEvents

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.

ParameterTypeDescription
recordModel

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.

ParameterTypeDescription
recordModel

Record whose row should be expanded

Configuration

applyDefaultsstaticBase

Events

Lifecycle

destroystaticBase

Misc

doDisableInstancePlugin
initClassstaticBase
isOfTypeNamestaticBase
mixinstaticBase
optionalLstaticLocalizable

Events

9

This 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 }) => {

});
ParameterTypeDescription
recordModel

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 }) => {

});
ParameterTypeDescription
recordModel

Record

This event fires when a row has finished collapsing.

// Adding a listener using the "on" method
rowExpander.on('rowCollapse', ({ record }) => {

});
ParameterTypeDescription
recordModel

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 }) => {

});
ParameterTypeDescription
recordModel

Record

expandedElementsObject

An object with the Grid region name as property and the expanded body element as value

widgetWidget

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 param instead.

widgetsObject

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

catchAllEvents
destroyEvents
disableInstancePlugin
enableInstancePlugin

Event handlers

9

This 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 }) {

    }
});
ParameterTypeDescription
recordModel

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 }) {

    }
});
ParameterTypeDescription
recordModel

Record

This event called when a row has finished collapsing.

new RowExpander({
    onRowCollapse({ record }) {

    }
});
ParameterTypeDescription
recordModel

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 }) {

    }
});
ParameterTypeDescription
recordModel

Record

expandedElementsObject

An object with the Grid region name as property and the expanded body element as value

widgetWidget

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 param instead.

widgetsObject

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

onDestroyEvents
onDisableInstancePlugin
onEnableInstancePlugin

Typedefs

1

CSS variables

6
NameDescription
--b-row-expander-border-bottom-widthRow expander body's border bottom width
--b-row-expander-font-weightRow expander body's font weight
--b-row-expander-backgroundRow expander body's background color
--b-row-expander-paddingRow expander body's padding
--b-row-expander-border-bottom-colorRow expander body's border bottom color
--b-row-expander-colorRow expander body's color