Angular Spreadsheet Component | ng-hub-ui-spreadsheet

Editable Angular spreadsheet with Excel's keyboard, rectangular selection, a clipboard that round-trips with Excel, frozen panes and merged cells. MIT, accessible, no paid tier.

API reference

Here's the full contract for spreadsheet: everything you can bind, listen to, project and theme, gathered in one place. Wire up what you need and style what you want — it's standalone and signal-friendly.

Inputs

Dial spreadsheet in with 31 inputs. Bind them like any Angular @Input.

NameTypeDefaultDescription
rowsRequiredreadonly TRow[]—The rows, as you hold them. The sheet never writes to them.
columnsRequiredreadonly HubSpreadsheetColumn<TRow>[]—What each column shows and allows.
rowKeyRequired(row: TRow) => string—A stable name per row, so a save state follows its row across a reload.
errorsRecord<string, string>{}A validation message per cell, keyed like `states`. Owned by the host, because validation belongs to whoever knows what the data means.
statesRecord<string, HubSpreadsheetCellState>{}Save state per cell, keyed by the row key and the column alias joined by a tab.
decimalMark',' | '.'','The character this reader types decimals with. Only consulted when a pasted value carried no raw number.
emptyTextstring''What an empty sheet says.
pageSizenumber10How many rows Page Up and Page Down travel.
readonlybooleanfalseTurns off every editor, whatever the individual cells say.
frozenColumnsnumber0How many columns stay pinned to the leading edge, counted the way Excel freezes panes.
frozenRowsnumber0How many rows stay pinned below the header.
spansreadonly HubGridSpan[][]Merged blocks, each named by its top-left corner and its extent.
fillHandlebooleanfalseWhether the sheet offers the fill handle at the corner of the selection.
canUndobooleanfalseWhether undo is available. The sheet only asks; the owner keeps the history.
canRedobooleanfalseWhether redo is available.
resizableColumnsbooleanfalseWhether a reader may drag a column's trailing edge to widen or narrow it.
reorderableColumnsbooleanfalseWhether a reader may drag a header to put the column somewhere else.
columnWidthsRecord<string, number>{}The width of each column in pixels, keyed by alias. Two-way: the sheet writes it as the reader drags, and you may persist it.
minColumnWidthnumber48The narrowest a column may be dragged.
virtualbooleanfalseDraws only the rows the viewport covers and reserves the height of the rest, for a sheet of thousands. Off by default, and it costs something: the rows out of view are not in the document, so the browser's own find and printing reach only what is drawn, and the rows have to be of one height. Turning it on also gives the sheet a height of 24rem, because a sheet left to grow is as tall as its content and then there is nothing to virtualise.
rowHeightnumber0How tall a row is while virtualising, in pixels. Nought measures the first row drawn, which is right whenever the rows are alike. Declare it for a sheet built while hidden: a row that has never been laid out measures nought, and a window worked out from nought is the whole sheet.
formulasbooleanfalseWhether a cell whose value starts with = is a formula. Off unless asked for: a column of text may perfectly well hold =1+1 as the thing somebody wrote. A formula can name a column by its alias — =[units] * [price], =SUM([total:]) — which goes on meaning the same after a column is moved or a header rewritten, or by coordinate the way a spreadsheet writes them — =ROUND(SUM(E1:E4), 2) — which says where a column was rather than which one it is; rewriteRowFormulas() moves those with the sheet, and a reference to something deleted becomes #REF! rather than quietly reading its neighbour. A colon reads a whole column and leaves out the cell asking for it; anything else that goes round in a circle is shown as #CYCLE! rather than chased.
mergeablebooleanfalseWhether the reader may put cells together and take them apart, offered in the context menu — so it needs contextMenu too. Off unless asked for, like every structural change.
editOn'click' | 'double-click''double-click'What opens the editor with the pointer. A double click by default, the spreadsheet convention: a click picks the cell and leaves its value alone, the first character typed opens the editor and lands in it, and a cell that holds a list or a date shows a mark that opens it on one click. Set click for a sheet that exists to be typed into. Either way the editor opens on release and only when the pointer did not travel, so dragging out a range still works.
contextMenubooleanfalseWhether a right-click offers the menu of structural changes. Each entry still has to pass the permissions, and the menu is never opened empty.
columns[].kind'text' | 'number' | 'currency' | 'date' | 'boolean' | 'select''text'How the column shows its values and reads back what is typed. A select column opens a list and refuses anything not on it; a boolean draws a checkbox that turns over in one click; a date opens a date field.
columns[].formulastring—A formula every cell of this column is worked out from, declared in code rather than held by the data — a line total, a tax, a running balance. The other way round from a formula a reader types, which is a value the row holds and can be changed like any other. Cells of such a column cannot be typed into at all: replacing one would lose the formula for that row alone, and nobody notices until the totals stop adding up. Needs formulas on the sheet.
columns[].placeholderstring—A hint shown in an empty cell of this column while the pointer rests on it. Never in place of a value: a column of hints always on screen reads as a column of content.
columns[].optionsreadonly HubSpreadsheetOption[]—The answers a select column offers. A cell shows the matching label rather than the stored value, and falls back to the raw value when nothing matches, so a row holding a retired option stays readable.
structureHubSpreadsheetStructureOptions<TRow>{}Which structural changes the reader may ask for. Everything is refused until allowed.
retiredColumnKeysreadonly string[][]Aliases of columns that were deleted, so a new one never inherits a dead identity.

Outputs

React to what spreadsheet does — 12 events to hook your logic onto.

NameTypeDescription
commitHubSpreadsheetCommit<TRow>A cell took a new value, and it differs from the one it held.
pastedHubSpreadsheetPaste<TRow>A block was pasted, with counts of what fell outside the sheet and what was not a number.
clearedHubSpreadsheetCellRef<TRow>[]Delete was pressed over a selection; carries the editable cells it covered.
selectionChangeHubGridRange | nullThe selected rectangle changed.
filledHubSpreadsheetPaste<TRow>A drag of the fill handle finished, reported as one event for the whole gesture.
undoRequestedvoidThe reader asked to undo.
redoRequestedvoidThe reader asked to redo.
columnMoved{ from: number; to: number; key: string; keys: string[] }A column was dropped somewhere else. Reported, not applied: the payload carries the aliases already reordered.
mergeRequestedHubSpreadsheetMergeRequestThe reader asked to merge a selection. Carries the block to create and the anchors of the blocks it swallows, because appending without dropping those leaves two blocks claiming the same cell. applySpanMerge() does it in a line.
unmergeRequestedreadonly HubGridCoords[]The reader asked to take apart the blocks their selection touches, named by their anchors. applySpanUnmerge() applies it.
insertRequestedHubSpreadsheetInsertRequestThe reader asked to add rows or columns.
deleteRequestedHubSpreadsheetDeleteRequestThe reader asked to remove rows or columns, naming the aliases at stake.

Methods

Drive spreadsheet from code — 17 methods on its programmatic surface.

NameSignatureReturnsDescription
sheetToXlsxsheetToXlsx<TRow>(rows, columns, options?: HubXlsxOptions): Uint8ArrayUint8ArrayThe sheet as a real .xlsx, as bytes. Figures stay figures and a date column becomes a date, which is what a CSV cannot carry. Writes what the cell shows — a list column exports its label, a formula its result — unless values: 'stored' asks for what the rows hold.
downloadXlsxdownloadXlsx(filename: string, bytes: Uint8Array): void—Hands the reader that workbook to save, adding .xlsx to the name if it is missing. Here because every host writes the same six lines and forgets to release the object URL.
xlsxToRecordsxlsxToRecords<TRow>(bytes, columns, options?: HubXlsxReadOptions): Promise<Array<Record<string, string>>>Promise<Array<Record<string, string>>>Reads a workbook's first sheet into rows keyed by column alias, undoing the two things a CSV never does: shared strings, and dates stored as a count of days. Asynchronous, because a real workbook is compressed and the decompressor is the platform's.
sheetToCsvsheetToCsv<TRow>(rows, columns, options?: HubCsvOptions): stringstringThe sheet as CSV. With no delimiter given it comes from the decimal mark — a comma cannot separate what it also punctuates — so a file read where figures are written 1.234,56 gets semicolons.
downloadTextdownloadText(filename: string, text: string, type?: string): void—Hands the reader a text file to save, with the byte-order mark that makes a spreadsheet open it as UTF-8 rather than as mojibake.
csvToRecordscsvToRecords<TRow>(text, columns, options?: HubCsvOptions): Array<Record<string, string>>Array<Record<string, string>>Reads a CSV into rows keyed by column alias, sniffing the separator on the first line rather than assuming it. Headings are matched by header first and alias second, ignoring case and surrounding space.
sheetValuessheetValues<TRow>(rows, columns, options?: HubSheetValuesOptions): HubSpreadsheetValue[][]HubSpreadsheetValue[][]What every cell holds, with the formulas worked out and a failed one spelled the way a spreadsheet spells it. What both exporters read, exposed for a host that writes its own file.
evaluateSheetevaluateSheet<TRow>(input: HubFormulaSheetInput<TRow>): HubFormulaSheetHubFormulaSheetRuns every formula in a sheet, in whatever order the dependencies demand, worked out lazily and remembered. A circle is caught the moment it closes rather than chased. Exported for a host that wants the answers without drawing them.
rewriteRowFormulasrewriteRowFormulas<TRow extends object>(rows, change: HubFormulaShapeChange): TRow[]TRow[]Moves every coordinate in the rows after the sheet changes shape: a column moved, or rows or columns inserted or deleted. A reference to something deleted becomes #REF! rather than quietly reading its neighbour. The host calls it, because the rows are the host's.
mergeRequestFormergeRequestFor(spans, range): HubSpreadsheetMergeRequest | nullHubSpreadsheetMergeRequest | nullThe block a selection would become, and the anchors it swallows; null for a single cell. The swallowed anchors are the part a host gets wrong on its own — appended without dropping them, two blocks claim the same cell.
applySpanMergeapplySpanMerge(spans, request: HubSpreadsheetMergeRequest): HubGridSpan[]HubGridSpan[]The list of blocks after that merge, with the swallowed ones dropped.
applySpanUnmergeapplySpanUnmerge(spans, anchors: readonly HubGridCoords[]): HubGridSpan[]HubGridSpan[]The list of blocks after those are taken apart.
spreadsheetRecordsspreadsheetRecords<TRow>(rows, columns): HubSpreadsheetRecord[]HubSpreadsheetRecord[]Every row as plain data keyed by column alias, so a consumer works with { price: 12, units: 3 } instead of mapping coordinates onto its own fields by position.
parseForColumnparseForColumn<TRow>(text, column, decimalMark: ',' | '.'): HubSpreadsheetValue | undefinedHubSpreadsheetValue | undefinedWhat a typed or pasted string is worth in a column: the value, null for an empty cell, and undefined for something the column cannot hold — which lets a caller refuse rather than store rubbish.
nextColumnKeynextColumnKey(taken: Iterable<string>, prefix?: string): stringstringAn alias that has never belonged to another column, so a new column never reuses a retired name and no stored reference can resolve to the wrong thing.
danglingColumnKeysdanglingColumnKeys<TRow>(referenced: Iterable<string>, columns): string[]string[]References that no longer resolve to a column, which is what a deletion would leave pointing at nothing.
provideHubSpreadsheetControlsprovideHubSpreadsheetControls(adapter: HubSpreadsheetControlsAdapter): EnvironmentProvidersEnvironmentProvidersRegisters a control adapter, so every list column opens with it — searchable, already open, with the character that opened the cell in its search box. Without one the sheet draws its native list, which is what keeps the library installable on its own.

Templates

Make it yours — 2 template slots let you project custom markup.

NameDescriptionExample
hubSpreadsheetEditorReplaces the editor of one column, named by its alias. The template is handed the value, a commit and cancel pair, and `seed` — the character that opened the cell when the reader opened it by typing, which belongs in a picker's search box. It stays open until it calls commit or cancel. The sheet takes no dependency on whatever you put inside.<ng-template hubSpreadsheetEditor="assignee" let-value let-seed="seed" let-commit="commit"> <hub-select [items]="people" autoOpen [initialSearchTerm]="seed ?? null" [ngModel]="value" (ngModelChange)="commit($event)" /> </ng-template>
hubSpreadsheetCellDraws one column's cells yourself while they are not being edited: a badge for a state, an avatar beside a name, a row of actions. The sibling of hubSpreadsheetEditor, which covers the other half of a cell's life. The clipboard still carries the value rather than the drawing, and the cell is still what the keyboard is on. Hand the rows over through hubSpreadsheetCellRows and the compiler knows what `row` is.<ng-template hubSpreadsheetCell="state" let-value> <hub-badge variant="soft" [color]="colourOf(value)">{{ labelOf(value) }}</hub-badge> </ng-template>