Editing Nested FlexSheet Group Rows in Angular
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, the rows are rendered as tree/group rows. These rows are read-only by default, so users can edit child rows but not the parent rows unless you explicitly make the rows editable.
Steps to Complete
- Install and import the Wijmo Angular FlexSheet modules.
- 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 cascade parent row edits to child rows.
- Add helper methods for inserting rows.
- Add sample data and styles.
Getting Started
Install and import the Wijmo Angular FlexSheet modules
npm install @mescius/wijmo @mescius/wijmo.grid @mescius/wijmo.grid.sheet @mescius/wijmo.angular2.grid.sheet @mescius/wijmo.styles
import { Component } from '@angular/core';
import { WjGridSheetModule } from '@mescius/wijmo.angular2.grid.sheet';
import * as wjGrid from '@mescius/wijmo.grid';
import * as wjSheet from '@mescius/wijmo.grid.sheet';
import '@mescius/wijmo.styles/wijmo.css';
import './app.component.css';
- The
WjGridSheetModulepackage provides Angular support for the<wj-flex-sheet>component. ThewjGridandwjSheetimports are used to create the backingFlexGridand add it to the FlexSheet.
Add the FlexSheet markup and row insertion buttons
<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
#flex
(initialized)="initializeFlexSheet(flex)">
</wj-flex-sheet>
</div>
- The
initializedevent gives access to the FlexSheet instance so it can be configured in code.
Initialize the FlexSheet and make tree/group rows editable
@Component({
standalone: true,
imports: [WjGridSheetModule],
selector: 'app-component',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private flexSheet: wjSheet.FlexSheet | null = null;
data = getSampleData();
childItemsPath = ['checks', 'earnings', 'earnings'];
initializeFlexSheet(flexSheet: wjSheet.FlexSheet): void {
this.flexSheet = flexSheet;
flexSheet.autoGenerateColumns = false;
this.updateFactoryFnToShowFormulaResult(flexSheet);
this.addCascadeEditHandler(flexSheet);
flexSheet.loadedRows.addHandler((s) => {
const activeSheet = s.selectedSheet as TreeSheet;
if (activeSheet?.grid?.childItemsPath) {
s.rows.forEach((row) => {
row.isReadOnly = false;
});
}
});
flexSheet.selectedSheetChanged.addHandler((s) => {
const activeSheet = s.selectedSheet as TreeSheet;
if (activeSheet?.grid) {
s.childItemsPath = activeSheet.grid.childItemsPath;
}
if (activeSheet?._collapseInfo) {
s.rows.forEach((row) => {
row.isCollapsed = !activeSheet._collapseInfo!.has(row.index);
});
}
});
flexSheet.groupCollapsedChanged.addHandler((s, e) => {
const activeSheet = s.selectedSheet as TreeSheet;
if (!activeSheet._collapseInfo) {
activeSheet._collapseInfo = new Set<number>();
}
if (s.rows[e.row].isCollapsed) {
activeSheet._collapseInfo.delete(e.row);
} else {
activeSheet._collapseInfo.add(e.row);
}
});
this.createTreeGridSheet(flexSheet);
flexSheet.collapseGroupsToLevel(0);
}
}
-
The key part is the
loadedRowshandler. It setsrow.isReadOnly = false, allowing users to edit parent/group rows in the FlexSheet.
Create a FlexGrid-backed sheet with childItemsPath
type TreeSheet = wjSheet.Sheet & {
grid?: wjGrid.FlexGrid;
_collapseInfo?: Set<number>;
};
private createTreeGridSheet(flexSheet: wjSheet.FlexSheet): void {
const grid = new wjGrid.FlexGrid(document.createElement('div'), {
childItemsPath: this.childItemsPath,
autoGenerateColumns: false,
columns: [
{ binding: 'name' },
{ binding: 'hours', dataType: 'Number', format: 'n2' },
{ binding: 'rate', dataType: 'Number', format: 'n2' }
],
itemsSource: this.data
});
const sheet = new wjSheet.Sheet(flexSheet, grid, 'TreeGridSheet');
flexSheet.sheets.push(sheet);
}
- Instead of using
addBoundSheet, this creates aFlexGridwith hierarchical data and passes it to theSheetconstructor. This allows FlexSheet to display nested rows usingchildItemsPath.
Display formula results when cells are not being edited
private updateFactoryFnToShowFormulaResult(flexSheet: wjSheet.FlexSheet): void {
const oldFn = flexSheet.cellFactory.updateCell;
flexSheet.cellFactory.updateCell = function (
panel: wjGrid.GridPanel,
r: number,
c: number,
cell: HTMLElement,
rng?: wjGrid.CellRange
): void {
oldFn.call(this, panel, r, c, cell, rng);
if (panel.cellType !== wjGrid.CellType.Cell) {
return;
}
const sheet = panel.grid as wjSheet.FlexSheet;
if (rng && !rng.isSingleCell) {
r = rng.row;
c = rng.col;
}
if (
sheet.editRange &&
sheet.editRange.containsRow(r) &&
sheet.editRange.containsColumn(c)
) {
return;
}
const data = sheet.getCellData(r, c, true);
if (typeof data === 'string' && data.startsWith('=')) {
cell.innerText = sheet.getCellValue(r, c, true);
}
};
}
- This keeps formulas editable while showing the calculated result when the cell is not actively being edited.
Optionally cascade parent row edits to child rows
private addCascadeEditHandler(flexSheet: wjSheet.FlexSheet): void {
const bindingsToCascade = new Set(['hours', 'rate']);
flexSheet.cellEditEnded.addHandler((s, e) => {
const row = s.rows[e.row];
const col = s.columns[e.col];
const binding = col.binding;
if (!row || !row.hasChildren || !binding || !bindingsToCascade.has(binding)) {
return;
}
const item = row.dataItem as TreeItem;
const value = item[binding];
updateChildren(
item,
row.level,
s.childItemsPath as string[],
binding,
value
);
s.collectionView.refresh();
});
}
function updateChildren(
item: TreeItem,
level: number,
childItemsPath: string[],
binding: string,
value: unknown
): void {
const childPath = childItemsPath[Math.min(level, childItemsPath.length - 1)];
const children = item[childPath] as TreeItem[] | undefined;
if (!Array.isArray(children)) {
return;
}
children.forEach((child) => {
child[binding] = value;
updateChildren(child, level + 1, childItemsPath, 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 helper methods for inserting rows
type InsertMode = 'above' | 'below' | 'child';
interface TreeItem {
name: string;
hours?: number;
rate?: number;
checks?: TreeItem[];
earnings?: TreeItem[];
[key: string]: unknown;
}
insertItem(mode: InsertMode): void {
if (!this.flexSheet) {
return;
}
insertItemAtIndex(
this.flexSheet,
this.flexSheet.selection.row,
mode,
{ name: 'newItem', hours: 5, rate: 5 }
);
}
function insertItemAtIndex(
grid: wjSheet.FlexSheet,
index: number,
mode: InsertMode,
newItem: TreeItem
): void {
const row = grid.rows[index];
if (!row) {
return;
}
const childItemsPath = grid.childItemsPath as string[];
if (mode === 'child') {
const item = row.dataItem as TreeItem;
const binding = childItemsPath[Math.min(row.level, childItemsPath.length - 1)];
if (!Array.isArray(item[binding])) {
item[binding] = [];
}
(item[binding] as TreeItem[]).unshift(newItem);
} else {
const parentCollection = getParentCollection(grid, row.index);
const rowIndex = parentCollection.indexOf(row.dataItem as TreeItem);
const delta = mode === 'above' ? 0 : 1;
parentCollection.splice(rowIndex + delta, 0, newItem);
}
grid.collectionView.refresh();
}
function getParentCollection(
grid: wjSheet.FlexSheet,
rowIndex: number
): TreeItem[] {
const row = grid.rows[rowIndex];
const childItemsPath = grid.childItemsPath as string[];
if (row.level === 0) {
return grid.collectionView.sourceCollection as TreeItem[];
}
for (let r = rowIndex - 1; r >= 0; r--) {
const parentRow = grid.rows[r];
if (parentRow.hasChildren && parentRow.level < row.level) {
const parentItem = parentRow.dataItem as TreeItem;
return parentItem[childItemsPath[parentRow.level]] as TreeItem[];
}
}
return grid.collectionView.sourceCollection as TreeItem[];
}
- 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(): TreeItem[] {
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, Angular 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!
