Skip to main content Skip to footer

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

  1. Install and import the Wijmo Angular FlexSheet modules.
  2. Add the FlexSheet markup and row insertion buttons.
  3. Initialize the FlexSheet and make tree/group rows editable.
  4. Create a FlexGrid-backed sheet with childItemsPath.
  5. Display formula results when cells are not being edited.
  6. Optionally cascade parent row edits to child rows.
  7. Add helper methods for inserting rows.
  8. 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 WjGridSheetModule package provides Angular support for the <wj-flex-sheet> component. The wjGrid and wjSheet imports are used to create the backing FlexGrid and 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 initialized event 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 loadedRows handler. It sets row.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 a FlexGrid with hierarchical data and passes it to the Sheet constructor. This allows FlexSheet to display nested rows using childItemsPath.

 

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 hours and rate are 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!

Andrew Peterson

Technical Engagement Engineer