Chapter 40 of 40

A spreadsheet in the browser: cell formulas, copy & paste, undo

Column letters, row numbers, =SUM(B2:B9), paste from Excel and Ctrl+Z – one call, MIT-licensed, formulas never run as code.

  • ⏱ 7 min
  • ▶ Video episode 040
  • git tag ep-040
  • needs @guiexpert/table@1.1.99

After this chapter you can

  • build a sheet with SpreadsheetFactory
  • write formulas with A1 references and 70 Excel functions
  • add a formula bar
  • insert and delete rows and columns – references follow
  • switch the parts on in an existing table: editing, clipboard, undo, cellFormulas
  • save the sheet as JSON and export it to Excel with formulas

Excel, but in your app

A budget, a quote, a price calculation: your users know how this works in Excel. They type over a cell, paste a block they copied from a workbook, write =SUM(B2:B9) and press Ctrl+Z when they got it wrong. Since @guiexpert/table@1.1.99 the table can do all of that – MIT-licensed, without a dependency, in every framework. One call builds the sheet:

src/budget.ts
const data: unknown[][] = [
["Item", "Jan", "Feb", "Mar", "Total"],
["Rent", 950, 950, 950, "=SUM(B2:D2)"],
["Food", 410, 385, 432, "=SUM(B3:D3)"],
["Transport", 89, 89, 120, "=SUM(B4:D4)"],
["Fun", 120, 60, 210, "=SUM(B5:D5)"],
["Sum", "=SUM(B2:B5)", "=SUM(C2:C5)", "=SUM(D2:D5)", "=SUM(E2:E5)"],
["Rent %", "=ROUND(B2/B6*100,1)", "=ROUND(C2/C6*100,1)", "=ROUND(D2/D6*100,1)", "=ROUND(E2/E6*100,1)"]
];
src/main.ts
const { tableModel, tableOptions } = SpreadsheetFactory.create({
data, // inputs: numbers, texts and formulas ("=…")
rowCount: 1_000, // 1,000 rows × A–Z – only the filled cells cost memory
columnWidth: 110
});
const listener = new EventAdapter();
listener.onUndoStackChanged = (evt: GeUndoEvent) => { undo.disabled = !evt.canUndo; };
const scope = new TableScope(ctx.host, tableModel, new SimpleDomService(), tableOptions, listener);
scope.firstInit();
const api: TableApi = scope.getApi();
Live A budget with formulas – 1,000 rows × A–Z
Screenshot: A budget with formulas – 1,000 rows × A–Z Screenshot: A budget with formulas – 1,000 rows × A–Z
Select B3, type 500 and press Enter: the totals follow. Type =B2*2 in F2 – arrow keys point at cells while you type. Copy B2:D5 and paste it into a workbook, or the other way round.

What one call switches on

SpreadsheetFactory.create() returns a model and the options for a table or a wrapper – exactly like TableFactory. The model has column letters, row numbers and a sparse body: a million empty rows cost nothing, only filled cells take memory. The options contain the preset spreadsheet: true, which stands for five parts you can also use one by one:

  • editing – type to overwrite, F2 to edit, Enter/Tab move, Delete clears, Ctrl+Enter fills the selection, Ctrl+arrow jumps to the edge of the data (chapter 31 (Inline editing));
  • clipboard – copy, cut and paste with Excel and Google Sheets (chapter 22 (Selection & copy));
  • undo – Ctrl+Z / Ctrl+Y for edits, paste, fill and structure changes;
  • cellFormulas – formulas with A1 references;
  • fillHandle – drag the square at the corner of the selection.
// an existing table: the same preset – or the parts you want
const tableOptions: TableOptionsIf = { ...new TableOptions(), spreadsheet: true };
const onlySome: TableOptionsIf = { ...new TableOptions(), spreadsheet: { cellFormulas: false, fillHandle: false } };

In an existing table with object rows the formulas address the data columns by letters A, B, C …; to show them above your labels, build the model with TableFactory.createTableModel({ …, columnLetters: true }). Columns that must not take formulas (an id, a free text) get cellFormulas: false in their ColumnDef.

Formulas with A1 references

A formula is a text that starts with = – in your data, typed by a user or pasted. References are B2, $B$2 (absolute) and ranges B2:D5; 70 Excel functions are built in (FORMULA_FUNCTION_INFO): SUM, AVERAGE, IF, ROUND, VLOOKUP, XLOOKUP, SUMIFS, TEXT, DATE and more. The sheet keeps a dependency graph: a changed cell recalculates only what depends on it. Errors look like in Excel: #DIV/0!, #REF!, #NAME?, a circular reference is #CIRC!.

tableApi.setCellInput(1, 5, "=B2*2"); // row 2, column E (column 0 = row numbers)
tableApi.getCellInput(1, 5); // "=B2*2"
tableApi.getCellValue(1, 5); // 1900
tableApi.getCellAddress(1, 5); // "E2"
tableApi.evaluateFormula("=SUM(B2:D2)"); // 2850 – without writing a cell

While a user types a formula the references are colored and framed in the table, arrow keys or a click point at cells (F4 toggles $) and a list suggests functions with a short description – also for screen readers.

A formula bar

The cell shows the result, the FormulaBar the formula of the active cell – and its address. Typing there writes the cell (one undo step), Escape restores it, an address like D10 or B2:C5 in the address box goes there. It is plain DOM, so it works with every wrapper: pass the TableApi of tableReady.

const barHost = document.createElement("div"); // above the table
ctx.host.before(barHost);
const bar = new FormulaBar(barHost, api); // address box + the formula of the active cell

Insert and delete rows and columns

TableApi.insertRows(), TableApi.deleteRows(), TableApi.insertColumns() and TableApi.deleteColumns() take the row number − 1 and the letter index – the position in the sheet, also while it is sorted. Every formula follows: =SUM(B2:B5) becomes =SUM(B2:B6) after a row inside, a deleted reference becomes #REF!. Each change is one undo step.

const rowOfActiveCell = () => {
const [row, col] = api.getActiveCell() ?? [0, 1];
const address = api.getCellAddress(row, Math.max(col, 1)) ?? "A1"; // "B3" – also when sorted
return Number(address.replace(/^[A-Z]+/, "")) - 1;
};
ctx.button("insert row", () => api.insertRows(rowOfActiveCell(), 1)); // =SUM(B2:B5) → =SUM(B2:B6)
ctx.button("delete row", () => api.deleteRows(rowOfActiveCell(), 1));
const undo = ctx.button("undo", () => api.undo());
undo.disabled = true;
ctx.button("Excel", () => api.downloadExcel("budget.xlsx", "", { cellFormulas: true })); // real formulas

The table has no context menu of its own – a right-click arrives as onContextmenu (chapter 23 (Events & context menu)); the showcase builds a small menu with cut, copy, insert and delete in about 40 lines of HTML and JavaScript.

Save and export

TableApi.getSheetData() returns rows × columns of inputs – the formulas, not their results – ready for JSON.stringify and your server; pass them back as data to load the sheet. The Excel export with { cellFormulas: true } writes real formulas, Excel calculates them when it opens the file.

localStorage.setItem("budget", JSON.stringify(tableApi.getSheetData()));
const saved = JSON.parse(localStorage.getItem("budget") ?? "[]") as unknown[][];
const { tableModel, tableOptions } = SpreadsheetFactory.create({ data: saved });
await tableApi.downloadExcel("budget.xlsx", "", { cellFormulas: true });

What is different from Excel

  • Function names are English, the separator is , (; is accepted), decimals use . in formulas; typed and pasted values follow the locale ("1,5" is 1.5 in German).
  • No cell formatting toolbar, no charts, no several sheets per table, no array formulas that spill.
  • Dates are Excel serial numbers inside formulas (=DATE(2026,10,17)+1 works), shown as dates.

Next

Back to the overview of everything new: What's new – or the checklist of the course: chapter 34 (Checklist & finale).

API used in this chapter

Showcases