Skip to main content Skip to footer

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

  1. Install and import the Wijmo Vue FlexSheet packages.
  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 update child rows when a parent row is edited.
  7. Add row insertion helpers.
  8. 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 initialized event 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 loadedRows handler. It sets row.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 a FlexGrid with hierarchical data and pass it to the Sheet constructor. This allows FlexSheet to display nested rows using childItemsPath.

 

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

Andrew Peterson

Technical Engagement Engineer