A tab that is a table, and rows that fold away

A workbook tab that holds RECORDS rather than cells: the Orders tab renders as the data grid, with its own headers, sorting, filtering and inline editing, beside ordinary cell sheets. Its records are projected into the workbook's cells, header row included, so the Summary tab reads it with plain formulas: SUM, SUMIF, SUMPRODUCT and VLOOKUP all reach across, because the formula engine is never told the tab is different. Summary also shows Data > Group, with three regional blocks folded under their subtotals and the numbered level buttons at the corner of the outline bar. Edit a Qty on Orders and every figure follows. (requires @svgrid/enterprise)

A live, editable Svelte 5 data grid example from the SvGrid gallery (Spreadsheet). See the SvGrid documentation for the full API.

About this example

A workbook tab bound to records rather than cells in a Svelte 5 spreadsheet: it renders as the data grid, with its own headers, sorting, filtering and inline editing, beside ordinary cell sheets. Its records are projected into the workbook's cells, so other tabs read it with plain formulas - SUM, SUMIF, SUMPRODUCT and VLOOKUP all reach across. The same sheet shows Excel-style row outlining: blocks grouped under their subtotals, a collapse button on each summary row and the numbered level buttons at the corner of the bar. Enterprise, in @svgrid/enterprise.

Two things a business workbook wants that a sheet of cells is bad at.

The Orders tab is BOUND: it holds records rather than cells, and it renders as the data grid, with its own headers, sorting, filtering and inline editing. Name it in gridSheets and that is the whole setup.

The interesting part is what happens next. The records are projected into the workbook's cells, header row and all, so the Summary tab reads the bound tab with ordinary formulas:

=SUM(Orders!C2:C7) the totals down the column =SUMPRODUCT(Orders!C2:C7, Orders!D2:D7) =VLOOKUP("Gadget", Orders!B2:D7, 2, FALSE)

Nothing in the formula engine, the dependency graph or the file writers is told that Orders is a different kind of tab. That is what makes a bound tab worth having rather than an embedded widget.

The Summary tab also shows the second thing: Data > Group. The three regional blocks are grouped under their subtotals, so each one folds away behind the button on its total row, and the numbered buttons at the corner of the bar show the whole sheet at one depth.

Try: edit a Qty on Orders and watch every figure on Summary follow. Sort Orders by Region. On Summary, click the minus beside a region to fold it, then press 1 at the top of the bar to fold them all and 2 to open them again. Save As: the groups ride into the .xlsx as Excel's own outline levels, and the bound tab goes out as the cells it projects.

Imports, features and API used

Imports: @svgrid/enterprise, @svgrid/enterprise/sheet

Frequently asked questions

How is a bound tab different from putting a grid next to a sheet?

A grid beside a sheet is a separate thing that the formulas cannot see. A bound tab is part of the workbook: its records are projected into the workbook's cells, header row included, so =SUM(Orders!C2:C7) and =VLOOKUP(...) on any other tab read it exactly as they read a cell sheet. The formula engine, the dependency graph and the file writers are never told the tab is different.

Can I put a formula on a bound tab?

No, on purpose. A bound tab's cells are a rendering of its records, so anything typed into them would be overwritten by the next projection. A sheet that needs formulas beside the data is a cell sheet reading the bound one across, which is how the Summary tab in this demo works.

Is editing a record slow if the tab is large?

No. The projection goes through the same write an ordinary edit uses, which ignores a write that changes nothing, so editing one field of one record rewrites one cell and recalculates only what read it, rather than rebuilding the sheet.

Do the groups survive a save?

Yes. Row and column outlines are stored the way the file stores them, a level per line, so they go into the .xlsx as Excel's own outlineLevel and collapsed attributes and come back the same. A row a collapsed group folds is kept apart from a row you hid by hand, so opening the group brings it back.

Related documentation

Related articles

Source code (498-sheet-bound-tabs.svelte)

<script lang="ts">
  /**
   * 498. A tab that is a table, and rows that fold away
   * ---------------------------------------------------
   * Two things a business workbook wants that a sheet of cells is bad at.
   *
   * The Orders tab is BOUND: it holds records rather than cells, and it
   * renders as the data grid, with its own headers, sorting, filtering and
   * inline editing. Name it in `gridSheets` and that is the whole setup.
   *
   * The interesting part is what happens next. The records are projected
   * into the workbook's cells, header row and all, so the Summary tab
   * reads the bound tab with ordinary formulas:
   *
   *   =SUM(Orders!C2:C7)                    the totals down the column
   *   =SUMPRODUCT(Orders!C2:C7, Orders!D2:D7)
   *   =VLOOKUP("Gadget", Orders!B2:D7, 2, FALSE)
   *
   * Nothing in the formula engine, the dependency graph or the file
   * writers is told that Orders is a different kind of tab. That is what
   * makes a bound tab worth having rather than an embedded widget.
   *
   * The Summary tab also shows the second thing: Data > Group. The three
   * regional blocks are grouped under their subtotals, so each one folds
   * away behind the button on its total row, and the numbered buttons at
   * the corner of the bar show the whole sheet at one depth.
   *
   * Try: edit a Qty on Orders and watch every figure on Summary follow.
   * Sort Orders by Region. On Summary, click the minus beside a region to
   * fold it, then press 1 at the top of the bar to fold them all and 2 to
   * open them again. Save As: the groups ride into the .xlsx as Excel's
   * own outline levels, and the bound tab goes out as the cells it
   * projects.
   */
  import { SvSheet, createWorkbook, createSheetDocument } from '@svgrid/enterprise'
  import { groupLines, emptyOutline } from '@svgrid/enterprise/sheet'

  type Order = { region: string; item: string; qty: number; price: number }

  const orders: Order[] = [
    { region: 'North', item: 'Widget', qty: 12, price: 9.5 },
    { region: 'North', item: 'Gadget', qty: 4, price: 24 },
    { region: 'South', item: 'Widget', qty: 7, price: 9.5 },
    { region: 'South', item: 'Sprocket', qty: 18, price: 3.25 },
    { region: 'West', item: 'Gadget', qty: 9, price: 24 },
    { region: 'West', item: 'Sprocket', qty: 22, price: 3.25 },
  ]

  // The Summary tab is ordinary cells. Every formula on it reaches across
  // to Orders, which is bound; the engine cannot tell.
  const summary: string[][] = [
    ['Regional summary', '', ''],
    ['North', '=SUMPRODUCT((Orders!A2:A7="North")*Orders!C2:C7*Orders!D2:D7)', 'revenue'],
    ['North units', '=SUMIF(Orders!A2:A7, "North", Orders!C2:C7)', ''],
    ['North subtotal', '=B2', ''],
    ['South', '=SUMPRODUCT((Orders!A2:A7="South")*Orders!C2:C7*Orders!D2:D7)', 'revenue'],
    ['South units', '=SUMIF(Orders!A2:A7, "South", Orders!C2:C7)', ''],
    ['South subtotal', '=B5', ''],
    ['West', '=SUMPRODUCT((Orders!A2:A7="West")*Orders!C2:C7*Orders!D2:D7)', 'revenue'],
    ['West units', '=SUMIF(Orders!A2:A7, "West", Orders!C2:C7)', ''],
    ['West subtotal', '=B8', ''],
    ['', '', ''],
    ['All regions', '=B4+B7+B10', 'the three subtotals'],
    ['Units in total', '=SUM(Orders!C2:C7)', 'straight off the bound tab'],
    ['Gadget price', '=VLOOKUP("Gadget", Orders!B2:D7, 3, FALSE)', 'a lookup across'],
  ]

  const wb = createWorkbook([
    { name: 'Orders', cells: [] },
    { name: 'Summary', cells: summary },
  ])
  const doc = createSheetDocument({ workbook: wb })

  const state = doc.get('Summary')
  const at = { rowIdAt: (i: number) => `r${i}`, columnIdAt: (i: number) => String.fromCharCode(65 + i) }
  state.formats.set([[0, 0, 0, 2]], { bold: true, fill: '#e2e8f0', color: '#0f172a' }, at)
  for (const r of [3, 6, 9]) state.formats.set([[r, 0, r, 1]], { bold: true }, at)
  state.formats.set([[11, 0, 13, 1]], { bold: true }, at)
  state.formats.set([[1, 1, 13, 1]], { numFmt: '#,##0.00' }, at)
  state.formats.set([[2, 1, 2, 1]], { numFmt: '#,##0' }, at)
  state.formats.set([[5, 1, 5, 1]], { numFmt: '#,##0' }, at)
  state.formats.set([[8, 1, 8, 1]], { numFmt: '#,##0' }, at)
  state.formats.set([[12, 1, 12, 1]], { numFmt: '#,##0' }, at)
  state.formats.set([[0, 2, 13, 2]], { color: '#64748b' }, at)
  state.widths.A = 180
  state.widths.B = 150
  state.widths.C = 220

  // The detail rows of each region, grouped under the subtotal that
  // follows them. The summary line is the one after the detail, which is
  // where Excel puts the collapse button.
  let rowOutline = emptyOutline()
  for (const [from, to] of [[1, 2], [4, 5], [7, 8]] as const) {
    rowOutline = groupLines(rowOutline, from, to)
  }
  state.outline = { rows: rowOutline, cols: emptyOutline() }
</script>

<SvSheet
  document={doc}
  height="100%"
  rows={18}
  columns={6}
  gridSheets={{
    Orders: {
      fields: [
        { field: 'region', label: 'Region', width: 120 },
        { field: 'item', label: 'Item', width: 140 },
        { field: 'qty', label: 'Qty', type: 'number', width: 90 },
        { field: 'price', label: 'Price', type: 'number', width: 110 },
      ],
      rows: orders,
      editable: true,
    },
  }}
/>

View this example on GitHub

More Spreadsheet examples

  • Spreadsheet + Ribbon bar - The whole Excel surface as one component, <SvSheet workbook={wb} />: a six-tab ribbon (Home, Insert, Formulas, Data, Review, View), the Name Box and fx bar, sheet tabs and the Sum / Average / Count status bar. Format cells, merge them, comment on them, validate what goes in, colour them by rule, filter the region, protect the sheet; every button is the same call as its shortcut, so Ctrl+B and the Bold button cannot drift. A two-sheet P&L with the formats travelling in the document.
  • Spreadsheet + formulas - Real formula engine inside the grid: cell refs (A1), ranges (A1:A10), SUM / AVG / IF / COUNTIF / ROUND, arithmetic, string concat, cycle detection.
  • Blank sheet - just type - An empty Excel-style sheet on a plain <SvGrid>: column-letter headers (A..Z), a built-in 1..N row gutter, a name box + formula bar with a browsable function picker, gridlines, range selection and a fill handle. A real HyperFormula engine underneath: type a literal or a formula like =SUM(B2:D2) / =IF(...) and every dependent cell recalculates live. Drag a row or column border to resize; right-click for Cut / Copy / Paste / Clear.
  • Excel keyboard shortcuts - The muscle memory a spreadsheet user arrives with: Ctrl+Arrow jumps to the edge of the data region (a run-boundary search, so it hops gaps rather than running to the end), Ctrl+Shift+Arrow extends there, Ctrl+A takes the current region then the sheet, Ctrl+Space / Shift+Space take the column or row, Ctrl+D / Ctrl+R fill, Ctrl+; stamps the date. One enableSheet() call. Ctrl+Z undoes a whole fill in one press, not one per cell.
  • Formula bar + cell formats - The Excel cell experience: a formula bar showing the RAW text behind the active cell (the grid shows 1,234.50, the bar shows =B2*C2) with function autocomplete and signature hints, a Name Box that jumps to an address, and number formats that live on the CELL rather than the column. Ctrl+Shift+4 for currency, Ctrl+B to bold. The format store keys on row id, so sorting does not leave formatting behind on the old index.