Master Rows
The Master Row Plugin enhances the RevoGrid grid component by enabling the creation and management of master-detail rows. It is an extremely complex and advanced feature, despite its seemingly simple nature. Read more below.
Source code
import { defineCustomElements } from '@revolist/revogrid/loader';
import {
CellColumnFocusVerifyPlugin,
ColumnStretchPlugin,
MasterRowPlugin,
TreeDataPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme, observeCurrentTheme } from '../../composables/useRandomData';
import {
createMasterColumns,
createMasterRowConfig,
createMasterRows,
createMasterTreeConfig,
type MasterProjectRow,
} from './row-master.shared';
import './row-master.scss';
defineCustomElements();
const plugins = [TreeDataPlugin, MasterRowPlugin, CellColumnFocusVerifyPlugin, ColumnStretchPlugin];
export function load(parentSelector: string, rows?: MasterProjectRow[]) {
const parent = document.querySelector(parentSelector);
if (!parent) return () => undefined;
const source = rows?.length ? rows : createMasterRows();
const container = document.createElement('section');
container.className = 'row-master-showcase';
container.setAttribute('aria-label', 'Row Master portfolio explorer');
const grid = document.createElement('revo-grid');
grid.className = 'row-master-grid';
const initialDarkTheme = currentTheme().isDark();
grid.theme = initialDarkTheme ? 'darkMaterial' : 'material';
grid.columns = createMasterColumns(source);
grid.plugins = plugins;
grid.masterRow = createMasterRowConfig();
grid.tree = createMasterTreeConfig();
grid.readonly = true;
grid.stretch = 'last';
grid.hideAttribution = true;
container.appendChild(grid);
parent.appendChild(container);
grid.source = source;
const disconnectTheme = observeCurrentTheme((isDark) => {
grid.theme = isDark ? 'darkMaterial' : 'material';
});
return () => {
disconnectTheme();
grid.remove();
container.remove();
};
}
<template>
<section class="row-master-showcase" aria-label="Row Master portfolio explorer">
<RevoGrid
class="row-master-grid"
:theme="darkTheme ? 'darkMaterial' : 'material'"
:source="rows"
:columns="columns"
:plugins="plugins"
:master-row.prop="masterRow"
:tree.prop="tree"
:readonly="true"
stretch="last"
hide-attribution
/>
</section>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import {
CellColumnFocusVerifyPlugin,
ColumnStretchPlugin,
MasterRowPlugin,
TreeDataPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme, observeCurrentTheme } from '../../composables/useRandomData';
import {
createMasterColumns,
createMasterRowConfig,
createMasterRows,
createMasterTreeConfig,
type MasterProjectRow,
} from './row-master.shared';
import './row-master.scss';
const props = defineProps<{ rows?: MasterProjectRow[] }>();
const rows = ref(props.rows?.length ? props.rows : createMasterRows());
const columns = createMasterColumns(rows.value);
const plugins = [TreeDataPlugin, MasterRowPlugin, CellColumnFocusVerifyPlugin, ColumnStretchPlugin];
const masterRow = createMasterRowConfig();
const tree = createMasterTreeConfig();
const darkTheme = ref(typeof window !== 'undefined' && currentTheme().isDark());
let disconnectTheme: (() => void) | undefined;
onMounted(() => {
disconnectTheme = observeCurrentTheme((isDark) => {
darkTheme.value = isDark;
});
});
onUnmounted(() => disconnectTheme?.());
</script>
import { useEffect, useMemo, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import {
CellColumnFocusVerifyPlugin,
ColumnStretchPlugin,
MasterRowPlugin,
TreeDataPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme, observeCurrentTheme } from '../../composables/useRandomData';
import {
createMasterColumns,
createMasterRowConfig,
createMasterRows,
createMasterTreeConfig,
type MasterProjectRow,
} from './row-master.shared';
import './row-master.scss';
export default function RowMaster({ rows }: { rows?: MasterProjectRow[] }) {
const initialSource = useMemo(() => rows?.length ? rows : createMasterRows(), [rows]);
const [source, setSource] = useState(initialSource);
const columns = useMemo(() => createMasterColumns(source), [source]);
const plugins = useMemo(() => [
TreeDataPlugin,
MasterRowPlugin,
CellColumnFocusVerifyPlugin,
ColumnStretchPlugin,
], []);
const masterRow = useMemo(() => createMasterRowConfig(), []);
const tree = useMemo(() => createMasterTreeConfig(), []);
const [darkTheme, setDarkTheme] = useState(() => currentTheme().isDark());
useEffect(() => observeCurrentTheme(setDarkTheme), []);
useEffect(() => setSource(initialSource), [initialSource]);
return (
<section className="row-master-showcase" aria-label="Row Master portfolio explorer">
<RevoGrid
className="row-master-grid"
theme={darkTheme ? 'darkMaterial' : 'material'}
source={source}
columns={columns}
plugins={plugins}
masterRow={masterRow}
tree={tree}
readonly={true}
stretch="last"
hideAttribution={true}
/>
</section>
);
}
import { Component, Input, NO_ERRORS_SCHEMA, OnDestroy, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
CellColumnFocusVerifyPlugin,
ColumnStretchPlugin,
MasterRowPlugin,
TreeDataPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme, observeCurrentTheme } from '../../composables/useRandomData';
import {
createMasterColumns,
createMasterRowConfig,
createMasterRows,
createMasterTreeConfig,
type MasterProjectRow,
} from './row-master.shared';
@Component({
selector: 'row-master-grid',
standalone: true,
imports: [RevoGrid],
encapsulation: ViewEncapsulation.None,
schemas: [NO_ERRORS_SCHEMA],
styleUrls: ['./row-master.scss'],
template: `
<section class="row-master-showcase" aria-label="Row Master portfolio explorer">
<revo-grid
class="row-master-grid"
[theme]="theme"
[source]="source"
[columns]="columns"
[plugins]="plugins"
[masterRow]="masterRow"
[tree]="tree"
[readonly]="true"
[stretch]="'last'"
[hideAttribution]="true"
></revo-grid>
</section>
`,
})
export class RowMasterGridComponent implements OnDestroy {
@Input()
set rows(value: MasterProjectRow[] | undefined) {
this.source = value?.length ? value : createMasterRows();
this.columns = createMasterColumns(this.source);
}
theme: HTMLRevoGridElement['theme'] = currentTheme().isDark() ? 'darkMaterial' : 'material';
private readonly disconnectTheme = observeCurrentTheme((isDark) => {
this.theme = isDark ? 'darkMaterial' : 'material';
});
readonly plugins = [TreeDataPlugin, MasterRowPlugin, CellColumnFocusVerifyPlugin, ColumnStretchPlugin];
readonly masterRow = createMasterRowConfig();
readonly tree = createMasterTreeConfig();
source = createMasterRows();
columns = createMasterColumns(this.source);
ngOnDestroy() {
this.disconnectTheme();
}
}
Table of Contents
Section titled “Table of Contents”Features
Section titled “Features”- Expandable Rows: Easily expand rows to reveal additional details.
- Custom Templates: Define how master rows are rendered using customizable templates.
Basic Setup
Section titled “Basic Setup”-
Import Necessary Plugins and Utilities:
import { EXPAND_COLUMN, MasterRowPlugin, CellColumnFocusVerifyPlugin } from '@revolist/revogrid-pro';import type { RowMasterConfig } from '@revolist/revogrid-pro';import './row-master.style.css'; -
Define Columns:
const columns: HTMLRevoGridElement['columns'] = [{prop: 'expand',...EXPAND_COLUMN,},// ...]; -
Initialize the Grid with Plugins:
const grid = document.querySelector('revo-grid');grid.source = makeData(10, 20);grid.columns = columns;// Define plugingrid.plugins = [MasterRowPlugin,];const masterRow: RowMasterConfig = {rowHeight: 100,template: (h, data) => {return data.model.subRows?.map((subRow: Person) =>h('span', { class: { 'master-row': true } }, [h('img', { src: subRow.avatar, width: '20', height: '20' }),`${subRow.firstName} ${subRow.lastName} `,]),);},};grid.masterRow = masterRow;}
Configuration
Section titled “Configuration”The Master Row Plugin can be customized by assigning a RowMasterConfig object to grid.masterRow. The primary configuration option is the template function, which defines how the master row content is rendered.
RowMasterConfig
Section titled “RowMasterConfig”| Property | Type | Description |
|---|---|---|
template | (h: Function, data: any) => JSX.Element | A function that returns the JSX structure for the master row. It receives a rendering function h and the data context data. |
preserveExpandedOnSourceChange | boolean | Defaults to false. Retain expanded details and local state after source replacement, using rowKey, the Tree Data ID field, or physical source positions. |
rowKey | string | Optional unique ID field. Overrides the automatic Tree Data ID field; ordinary grids otherwise use physical source positions. |
allowDetailEvents | readonly string[] | Event names from the master detail area that are allowed to bubble to the parent grid. All RevoGrid events inside the detail area are stopped by default. |
Custom Templates
Section titled “Custom Templates”Customize the master row by defining a template function that returns the desired JSX structure. This allows for flexible and dynamic rendering based on the row data.
const masterRow: RowMasterConfig = { template: (h, data) => { return data.model.subRows?.map((subRow: Person) => h('div', { class: { 'custom-master-row': true } }, [ h('img', { src: subRow.avatar, width: '30', height: '30' }), `${subRow.firstName} ${subRow.lastName}`, ]), ); },};To let selected events from nested detail content reach the parent grid, add them by name:
const masterRow: RowMasterConfig = { allowDetailEvents: ['afteredit'], template: (h, data) => h('revo-grid', { source: data.model.details }),};Programmatic Expand and Collapse
Section titled “Programmatic Expand and Collapse”EXPAND_COLUMN toggles a single master row by dispatching the ROW_MASTER event with the current cell template payload. You can dispatch the same event directly when a toolbar, external control, or custom cell template needs to control master-row state.
For one row, pass the same cell template payload shape used by cell templates. The plugin treats this as a toggle:
import { ROW_MASTER } from '@revolist/revogrid-pro';
grid.dispatchEvent(new CustomEvent(ROW_MASTER, { detail: cellTemplateData,}));For multiple rows, pass a batch payload with a rows array. Each row target declares its desired final state with expanded: true or expanded: false, so one event can open and close different rows at the same time:
import { ROW_MASTER } from '@revolist/revogrid-pro';import type { RowMasterBatchEvent } from '@revolist/revogrid-pro';
const detail: RowMasterBatchEvent = { rows: [ { rowType: 'rgRow', rowIndex: 1, expanded: true }, { rowType: 'rgRow', rowIndex: 3, expanded: false }, { rowType: 'rowPinStart', rowIndex: 0, expanded: true }, ],};
grid.dispatchEvent(new CustomEvent(ROW_MASTER, { detail }));rowIndex is the current virtual row index for the given rowType. The plugin converts every target to a stable internal row reference before applying the batch, so one event can safely mix opened and closed rows without later changes shifting earlier targets. If multiple targets resolve to the same row, the last entry wins.
Preserve or Reset Detail State
Section titled “Preserve or Reset Detail State”Choose whether a source update should keep the current detail view or start a fresh one.
These options apply to MasterRowPlugin and grid.masterRow.
Keep expanded rows and local input state
Section titled “Keep expanded rows and local input state”Enable preserveExpandedOnSourceChange to keep expanded details and local input state when you
replace the source. For ordinary grids, details stay at the same source positions within each row type.
Tree Data automatically follows its ID field (tree.idField, or id by default):
grid.masterRow = { rowHeight: 180, preserveExpandedOnSourceChange: true, template: (h, data) => h('div', null, [ h('strong', null, data.model.name), h('input', { type: 'text', 'aria-label': 'Draft note', placeholder: 'Type a draft note', }), ]),};
// Refresh with new row objects.grid.source = grid.source.map(row => ({ ...row }));Open a detail, type a note, then refresh the source. The detail at that position stays expanded and its uncontrolled input keeps its value. Scrolling out of view and back also retains local state.
With positional matching, inserting or reordering records keeps the draft at its source position, so it may belong to a different record. Sorting the displayed rows does not change their physical source indexes.
Follow records using an ID field
Section titled “Follow records using an ID field”Set rowKey when expanded details should follow their records across source reordering or API refreshes:
grid.masterRow = { ...grid.masterRow, preserveExpandedOnSourceChange: true, rowKey: 'id',};
// New objects in a different order; details follow matching IDs.grid.source = [...grid.source].reverse().map(row => ({ ...row }));IDs must be unique strings or finite numbers in each row type. Missing, invalid, or duplicate IDs
are not preserved; they do not fall back to source positions. Removed or hidden records lose their details.
Tree Data uses its ID field automatically, including when activated by a tree column. An explicit
rowKey takes precedence. Without either a custom field or Tree Data, matching uses physical source indexes.
Preserving detail state does not save input text into your data model. Store drafts in application state if they must survive explicit collapse, removal of a record, grid destruction, or a page reload. For controlled inputs and nested components, their own value/prop update rules still apply.
Recreate details from updated data
Section titled “Recreate details from updated data”Leave preserveExpandedOnSourceChange unset, or set it to false, to collapse expanded rows and
remove their detail DOM whenever the source is replaced. Opening a row again renders its template
with the updated model and starts a fresh detail view. Do not cancel source cleanup in a
BEFORE_ROW_MASTER_COLLAPSE listener when you want this reset behavior:
grid.masterRow = { ...grid.masterRow, preserveExpandedOnSourceChange: false,};
// New records, for example the result of a server refresh.grid.source = grid.source.map(row => ({ ...row, status: 'Updated' }));If you want a fresh detail to open automatically, dispatch ROW_MASTER after the source update.
This example opens the first row of an ordinary, nonempty, unfiltered source:
import { ROW_MASTER } from '@revolist/revogrid-pro';
grid.addEventListener('afteranysource', () => { grid.dispatchEvent(new CustomEvent(ROW_MASTER, { detail: { rows: [{ rowType: 'rgRow', rowIndex: 0, expanded: true }], }, }));}, { once: true });
grid.source = grid.source.map(row => ({ ...row, status: 'Updated' }));Use the target row’s current virtual index when sorting, filtering, or Tree Data changes its position. To reset just one detail without replacing the source, explicitly collapse it and expand it again using the programmatic controls.
Automatic Collapse
Section titled “Automatic Collapse”Expanded master rows are cleared when the grid is about to change row visibility or row structure in a way that can invalidate the detail overlay position.
The plugin automatically cleans up expanded rows when:
- the grid source is replaced, unless
preserveExpandedOnSourceChangeis enabled - filtering is about to trim rows through
beforefiltertrimmed - row dragging starts
During that cleanup, expanded master rows are collapsed, detail overlay nodes are removed, duplicated detail rows are removed from the row item lists, and custom detail row heights are cleared. This prevents a filtered or replaced row from leaving stale detail height behind after the row itself is no longer visible.
For source updates, prefer preserveExpandedOnSourceChange to retain details after a refresh. The same positional or rowKey matching rules apply when source cleanup is canceled through the event. To cancel automatic cleanup through an event instead, listen for BEFORE_ROW_MASTER_COLLAPSE and call preventDefault():
import { BEFORE_ROW_MASTER_COLLAPSE } from '@revolist/revogrid-pro';
grid.addEventListener(BEFORE_ROW_MASTER_COLLAPSE, (event) => { if (event.detail.reason === 'filter') { event.preventDefault(); }});When preserveExpandedOnSourceChange is enabled, source updates use preservation directly and do not emit this collapse event. The event detail includes reason ('source', 'filter', or 'drag') and rows, the expanded master rows that are about to be cleared.
Preventing the default cleanup means your application is responsible for handling any stale detail overlays or custom row heights if the related source row is no longer visible.
API Reference
Section titled “API Reference”MasterRowPlugin
Section titled “MasterRowPlugin”A plugin class that manages master-detail rows within the RevoGrid component.
Methods
Section titled “Methods”-
constructor(revogrid: HTMLRevoGridElement, providers: PluginProviders)
Initializes the plugin with the RevoGrid instance and plugin providers.
-
expandRow(type: DimensionRows, virtRowIndex: number)
Expands a specific row to show its master details.
-
collapseRow(type: DimensionRows, rowVIndexes: number[])
Collapses one or more rows, hiding their master details.
-
destroy()
Cleans up event listeners and other resources when the plugin is destroyed.
Events
Section titled “Events”The plugin listens to various RevoGrid events to manage the state of master rows, including:
AFTER_GRID_RENDER_EVENTBEFORE_ROW_RENDER_EVENTBEFORE_ROW_MASTER_COLLAPSE- cancelable event fired before automatic master-row cleanup collapses expanded rows.ROW_MASTER- toggles one row from a cell template payload, or applies a batch of desired row states with{ rows: [{ rowType, rowIndex, expanded }] }.OVERLAY_NODE- …and more.