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 参考

这里是 spreadsheet 的完整契约:你可以绑定、监听、投影和设置主题的一切,都集中在一处。接入你需要的部分,自定义你想要的样式——它是 standalone 的,且对 signal 友好。

输入

通过 31 个输入项精细调校 spreadsheet。像绑定任意 Angular @Input 一样绑定它们。

名称类型默认值描述
rows必填readonly TRow[]—The rows, as you hold them. The sheet never writes to them.
columns必填readonly HubSpreadsheetColumn<TRow>[]—What each column shows and allows.
rowKey必填(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.
virtualbooleanfalse只画视口覆盖到的行,并为其余的行预留高度,适合成千上万行的表。默认关闭,而且有代价:视野之外的行不在文档里,浏览器自带的查找和打印只能触及画出来的部分,且各行高度必须一致。开启时还会给表 24rem 的高度,因为任其生长的表和内容一样高,那样就没有东西可虚拟化了。
rowHeightnumber0虚拟化时一行的高度,单位像素。0 表示量第一行画出来的行;只要各行相仿就没问题。表若是在隐藏状态下搭起来的,请明确写出:从未布局过的行量出来是 0,而从 0 推出的窗口就是整张表。
formulasbooleanfalse值以 = 开头的单元格算不算公式。不主动开启:文本列完全可能就把 =1+1 当作某人写下的内容。公式可以用别名指代列——=[units] * [price]、=SUM([total:])——挑动列或改写表头之后意思依旧;也可以用表格软件那种坐标——=ROUND(SUM(E1:E4), 2)——坐标说的是列“在哪里”而不是“是哪一列”,所以 rewriteRowFormulas() 会让它们随表格的形状一起移动,而指向已删除内容的引用会变成 #REF!,而不是默默读旁边一列。冒号读取整列并排除发问的那个单元格;其他的环会显示为 #CYCLE!,而不是一路追下去。
mergeablebooleanfalse读者能否合并单元格并再拆开,通过右键菜单提供,所以也需要 contextMenu。和所有改变结构的功能一样,不主动开启。
editOn'click' | 'double-click''double-click'用指针打开编辑器的方式。默认是双击,即表格的惯例:单击只选中单元格,不动它的值;键入的第一个字符会打开编辑器并落在里面;带列表或日期的单元格会显示一个标记,单击它即可打开。设为 click 则第一次单击就打开,适合本来就是用来录入的表。两种方式下编辑器都在松开时打开,且仅在指针没有移动时,所以拖出区域照常可用。
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—本列每个单元格据以算出的公式,写在代码里而不是存在数据中:行小计、税额、滚动余额。与读者敲入的公式正好相反,那种是行持有的值,可以像别的值一样改。这种列的单元格根本打不开:替换掉一个会只在那一行丢掉公式,而这要等到合计对不上才会被发现。需要表上的 formulas。
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.

输出

响应 spreadsheet 的行为——有 12 个事件可供你挂接逻辑。

名称类型描述
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.
mergeRequestedHubSpreadsheetMergeRequest读者要求合并一片选区。负载里有要创建的合并块,以及它吞掉的那些块的锚点:只追加而不移除它们,就会有两个块争同一个单元格。applySpanMerge() 一行搞定。
unmergeRequestedreadonly HubGridCoords[]读者要求拆开选区碰到的合并块,以锚点指明。用 applySpanUnmerge() 应用。
insertRequestedHubSpreadsheetInsertRequestThe reader asked to add rows or columns.
deleteRequestedHubSpreadsheetDeleteRequestThe reader asked to remove rows or columns, naming the aliases at stake.

方法

通过代码驱动 spreadsheet——其编程接口提供 17 个方法。

名称签名返回值描述
sheetToXlsxsheetToXlsx<TRow>(rows, columns, options?: HubXlsxOptions): Uint8ArrayUint8Array把表格写成真正的 .xlsx,返回字节。数字仍是数字,日期列就是日期,这是 CSV 装不下的。写出的是单元格显示的内容:列表列导出它的标签,公式导出它的结果;要行里存的原值就传 values: 'stored'。
downloadXlsxdownloadXlsx(filename: string, bytes: Uint8Array): void—把那个工作簿交给读者保存,名字里缺 .xlsx 就补上。放在这里是因为每个宿主都要写同样的六行,而且都忘了释放 object URL。
xlsxToRecordsxlsxToRecords<TRow>(bytes, columns, options?: HubXlsxReadOptions): Promise<Array<Record<string, string>>>Promise<Array<Record<string, string>>>把工作簿的第一张表读成以列别名为键的行,并还原 CSV 里根本不存在的两件事:共享字符串,以及以天数保存的日期。它是异步的,因为真正的工作簿是压缩的,解压用的是平台自带的能力。
sheetToCsvsheetToCsv<TRow>(rows, columns, options?: HubCsvOptions): stringstring把表格写成 CSV。不指定分隔符时由小数点符号决定——逗号不能既当小数点又当分隔符——所以给写 1.234,56 的人看的文件用分号。
downloadTextdownloadText(filename: string, text: string, type?: string): void—把一个文本文件交给读者保存,带上让表格软件按 UTF-8 打开的字节序标记,不会变成乱码。
csvToRecordscsvToRecords<TRow>(text, columns, options?: HubCsvOptions): Array<Record<string, string>>Array<Record<string, string>>把 CSV 读成以列别名为键的行,分隔符是从第一行嗅出来的,不是猜的。表头先按表头、再按别名匹配,忽略大小写和两端空格。
sheetValuessheetValues<TRow>(rows, columns, options?: HubSheetValuesOptions): HubSpreadsheetValue[][]HubSpreadsheetValue[][]每个单元格的内容,公式已经算好,算不出来的按表格软件的写法写。两个导出读的都是它,公开出来给要自己写文件的宿主用。
evaluateSheetevaluateSheet<TRow>(input: HubFormulaSheetInput<TRow>): HubFormulaSheetHubFormulaSheet按依赖要求的顺序跑完表里所有公式,用到才算,算过就记住。环一闭合就被抓住,不会一路追下去。公开出来给只要结果、不需要绘制的宿主用。
rewriteRowFormulasrewriteRowFormulas<TRow extends object>(rows, change: HubFormulaShapeChange): TRow[]TRow[]表格变形之后——列挪了位置,或者插入、删除了行列——把行里所有坐标都挪过去。指向已删除内容的引用会变成 #REF!,而不是默默去读旁边一列。由宿主调用,因为行是宿主的。
mergeRequestFormergeRequestFor(spans, range): HubSpreadsheetMergeRequest | nullHubSpreadsheetMergeRequest | null一个选区会变成的合并块,以及它吞掉的锚点;只有一个单元格时返回 null。被吞掉的锚点正是宿主自己容易做错的地方:加了块却不去掉它们,两个块就会争同一个单元格。
applySpanMergeapplySpanMerge(spans, request: HubSpreadsheetMergeRequest): HubGridSpan[]HubGridSpan[]这次合并之后的块列表,被吞掉的已经去掉。
applySpanUnmergeapplySpanUnmerge(spans, anchors: readonly HubGridCoords[]): HubGridSpan[]HubGridSpan[]把那些块拆开之后的列表。
spreadsheetRecordsspreadsheetRecords<TRow>(rows, columns): HubSpreadsheetRecord[]HubSpreadsheetRecord[]每一行都是以列别名为键的纯数据,于是可以直接用 { price: 12, units: 3 },而不用按位置把坐标对到自己的字段上。
parseForColumnparseForColumn<TRow>(text, column, decimalMark: ',' | '.'): HubSpreadsheetValue | undefinedHubSpreadsheetValue | undefined一段敲进去或粘进来的文字在某一列里值多少:值本身,空单元格是 null,这一列装不了的东西是 undefined——于是调用方可以拒绝,而不是把垃圾存下来。
nextColumnKeynextColumnKey(taken: Iterable<string>, prefix?: string): stringstring一个从来不属于别的列的别名。新列不会重用退役的名字,任何已保存的引用也就不可能指到别处去。
danglingColumnKeysdanglingColumnKeys<TRow>(referenced: Iterable<string>, columns): string[]string[]已经解析不到任何列的引用,也就是一次删除会留下的悬空指向。
provideHubSpreadsheetControlsprovideHubSpreadsheetControls(adapter: HubSpreadsheetControlsAdapter): EnvironmentProvidersEnvironmentProviders注册一个控件适配器,之后每个列表列都用它打开——带搜索、已经展开,打开单元格的那个字符也落在搜索框里。没有适配器时表格画自己的原生列表,这正是这个库能单独安装的原因。

模板

打造你自己的样式——2 个模板插槽让你可以投影自定义标记。

名称描述示例
hubSpreadsheetEditor替换某一列的编辑器,按别名指定。模板会拿到值、commit 与 cancel 这一对,以及 `seed`——读者以键入方式打开单元格时的那个字符,它该进选择器的搜索框。模板在调用其中之一之前保持打开;表格不依赖你放进去的东西。<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>
hubSpreadsheetCell在不处于编辑状态时,自己来画某一列的单元格:状态徽章、名字旁的头像、一排操作。它和 hubSpreadsheetEditor 是一对,后者管单元格的另一半时间。剪贴板带走的仍是值而不是画面,键盘落点也仍是单元格。用 hubSpreadsheetCellRows 把行交出去,编译器就知道 `row` 是什么。<ng-template hubSpreadsheetCell="state" let-value> <hub-badge variant="soft" [color]="colourOf(value)">{{ labelOf(value) }}</hub-badge> </ng-template>