Editing Nested FlexSheet Group Rows in Vue
Background
FlexSheet can display hierarchical data by creating a Wijmo FlexGrid with childItemsPath, then using that grid as the source for a FlexSheet Sheet.
When childItemsPath is used, parent rows are rendered as tree/group rows. These rows are read-only by default, so users can edit child rows but not parent rows unless the row-level read-only setting is cleared.
This article shows how to display nested rows in a Vue FlexSheet, allow editing on parent/group rows, preserve formula display behavior, and optionally update child rows when a parent row value changes.
Steps to Complete
- Install and import the Wijmo Vue FlexSheet packages.
- Add the FlexSheet markup and row insertion buttons.
- Initialize the FlexSheet and make tree/group rows editable.
- Create a FlexGrid-backed sheet with
childItemsPath. - Display formula results when cells are not being edited.
- Optionally update child rows when a parent row is edited.
- Add row insertion helpers.
- Add sample data and styles.
Getting Started
Install and import the Wijmo Vue FlexSheet packages
npm install @mescius/wijmo @mescius/wijmo.grid @mescius/wijmo.grid.sheet @mescius/wijmo.vue2.grid.sheet @mescius/wijmo.styles
<script setup>
import { ref } from 'vue';
import { WjFlexSheet } from '@mescius/wijmo.vue2.grid.sheet';
import * as wjGrid from '@mescius/wijmo.grid';
import * as wjSheet from '@mescius/wijmo.grid.sheet';
</script>
- The wijmo.vue2.grid.sheet package provides Vue components for FlexSheet.
Add the FlexSheet markup and row insertion buttons
<template>
<div class="container-fluid">
<div class="toolbar">
<button type="button" @click="insertItem('above')">Add above</button>
<button type="button" @click="insertItem('below')">Add below</button>
<button type="button" @click="insertItem('child')">Add child</button>
</div>
<wj-flex-sheet :initialized="initializeFlexSheet" />
</div>
</template>
- The
initializedevent gives access to the FlexSheet instance so it can be configured after it is created.
Initialize the FlexSheet and make tree/group rows editable
const flexSheet = ref(null);
const data = ref(getSampleData());
const childItemsPath = ['checks', 'earnings', 'earnings'];
function initializeFlexSheet(flex) {
flexSheet.value = flex;
flex.autoGenerateColumns = false;
updateFactoryFnToShowFormulaResult(flex);
addCascadeEditHandler(flex);
flex.loadedRows.addHandler((s) => {
const activeSheet = s.selectedSheet;
if (activeSheet && activeSheet.grid && activeSheet.grid.childItemsPath) {
s.rows.forEach((row) => {
row.isReadOnly = false;
});
}
});
flex.selectedSheetChanged.addHandler((s) => {
const activeSheet = s.selectedSheet;
if (activeSheet && activeSheet.grid) {
s.childItemsPath = activeSheet.grid.childItemsPath;
}
if (activeSheet && activeSheet._collapseInfo) {
s.rows.forEach((row) => {
row.isCollapsed = !activeSheet._collapseInfo.has(row.index);
});
}
});
flex.groupCollapsedChanged.addHandler((s, e) => {
const activeSheet = s.selectedSheet;
if (!activeSheet._collapseInfo) {
activeSheet._collapseInfo = new Set();
}
if (s.rows[e.row].isCollapsed) {
activeSheet._collapseInfo.delete(e.row);
} else {
activeSheet._collapseInfo.add(e.row);
}
});
createTreeGridSheet(flex);
flex.collapseGroupsToLevel(0);
}
-
The important part is the
loadedRowshandler. It setsrow.isReadOnly = false, which allows users to edit parent/group rows in the FlexSheet.
Create a FlexGrid-backed sheet with childItemsPath
function createTreeGridSheet(flex) {
const grid = new wjGrid.FlexGrid(document.createElement('div'), {
childItemsPath,
autoGenerateColumns: false,
columns: [
{ binding: 'name' },
{ binding: 'hours', dataType: 'Number', format: 'n2' },
{ binding: 'rate', dataType: 'Number', format: 'n2' }
],
itemsSource: data.value
});
const sheet = new wjSheet.Sheet(flex, grid, 'TreeGridSheet');
flex.sheets.push(sheet);
}
- Instead of using
addBoundSheet, create aFlexGridwith hierarchical data and pass it to theSheetconstructor. This allows FlexSheet to display nested rows usingchildItemsPath.
Display formula results when cells are not being edited
function updateFactoryFnToShowFormulaResult(flex) {
const oldFn = flex.cellFactory.updateCell;
flex.cellFactory.updateCell = function(panel, r, c, cell, rng) {
oldFn.call(this, panel, r, c, cell, rng);
if (panel.cellType !== wjGrid.CellType.Cell) {
return;
}
const grid = panel.grid;
if (rng && !rng.isSingleCell) {
r = rng.row;
c = rng.col;
}
if (
grid.editRange &&
grid.editRange.containsRow(r) &&
grid.editRange.containsColumn(c)
) {
return;
}
const cellData = grid.getCellData(r, c, true);
if (typeof cellData === 'string' && cellData.startsWith('=')) {
cell.innerText = grid.getCellValue(r, c, true);
}
};
}
- This keeps formulas editable while showing the calculated value when the cell is not actively being edited.
Optionally update child rows when a parent row is edited
function addCascadeEditHandler(flex) {
const bindingsToCascade = new Set(['hours', 'rate']);
flex.cellEditEnded.addHandler((s, e) => {
const row = s.rows[e.row];
const col = s.columns[e.col];
const binding = col.binding;
if (!row || !row.hasChildren || !bindingsToCascade.has(binding)) {
return;
}
const value = row.dataItem[binding];
updateChildren(
row.dataItem,
row.level,
s.childItemsPath,
binding,
value
);
s.collectionView.refresh();
});
}
function updateChildren(item, level, paths, binding, value) {
const childPath = paths[Math.min(level, paths.length - 1)];
const children = item[childPath];
if (!Array.isArray(children)) {
return;
}
children.forEach((child) => {
child[binding] = value;
updateChildren(child, level + 1, paths, binding, value);
});
}
- Use this when a parent row edit should push the edited value down to its descendants. In this example, edits to
hoursandrateare cascaded.
Add row insertion helpers
function insertItem(mode) {
if (!flexSheet.value) {
return;
}
insertItemAtIndex(
flexSheet.value,
flexSheet.value.selection.row,
mode,
{ name: 'newItem', hours: 5, rate: 5 }
);
}
function insertItemAtIndex(grid, index, mode, newItem) {
const row = grid.rows[index];
if (!row) {
return;
}
if (mode === 'child') {
const item = row.dataItem;
const binding = grid.childItemsPath[
Math.min(row.level, grid.childItemsPath.length - 1)
];
if (!Array.isArray(item[binding])) {
item[binding] = [];
}
item[binding].unshift(newItem);
} else {
const parentCollection = getParentCollection(grid, row.index);
const itemIndex = parentCollection.indexOf(row.dataItem);
const delta = mode === 'above' ? 0 : 1;
parentCollection.splice(itemIndex + delta, 0, newItem);
}
grid.collectionView.refresh();
}
function getParentCollection(grid, rowIndex) {
const row = grid.rows[rowIndex];
if (row.level === 0) {
return grid.collectionView.sourceCollection;
}
for (let r = rowIndex - 1; r >= 0; r--) {
const parentRow = grid.rows[r];
if (parentRow.hasChildren && parentRow.level < row.level) {
return parentRow.dataItem[grid.childItemsPath[parentRow.level]];
}
}
return grid.collectionView.sourceCollection;
}
- These helpers allow users to add rows above, below, or as children of the selected row while keeping the nested source collection synchronized.
Add sample data and styles
function getSampleData() {
return [
{
name: 'Jack Smith',
checks: [
{
name: 'check1',
earnings: [
{ name: 'hourly', hours: 30, rate: 15 },
{ name: 'overtime', hours: 10, rate: 20 },
{ name: 'bonus', hours: 5, rate: 30 }
]
},
{
name: 'check2',
earnings: [
{ name: 'hourly', hours: 20, rate: 18 },
{ name: 'overtime', hours: 20, rate: 24 }
]
}
]
}
];
}
.toolbar {
margin-bottom: 8px;
}
.toolbar button {
margin-right: 6px;
}
.wj-flexsheet {
height: 500px;
}
.wj-grouppanel .wj-remove {
display: none;
}
With this setup, Vue FlexSheet can display nested data, allow edits on group rows, preserve formula display behavior, and update child rows from parent edits when needed.
Happy coding!
