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. |
errors | Record<string, string> | {} | A validation message per cell, keyed like `states`. Owned by the host, because validation belongs to whoever knows what the data means. |
states | Record<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. |
emptyText | string | '' | What an empty sheet says. |
pageSize | number | 10 | How many rows Page Up and Page Down travel. |
readonly | boolean | false | Turns off every editor, whatever the individual cells say. |
frozenColumns | number | 0 | How many columns stay pinned to the leading edge, counted the way Excel freezes panes. |
frozenRows | number | 0 | How many rows stay pinned below the header. |
spans | readonly HubGridSpan[] | [] | Merged blocks, each named by its top-left corner and its extent. |
fillHandle | boolean | false | Whether the sheet offers the fill handle at the corner of the selection. |
canUndo | boolean | false | Whether undo is available. The sheet only asks; the owner keeps the history. |
canRedo | boolean | false | Whether redo is available. |
resizableColumns | boolean | false | Whether a reader may drag a column's trailing edge to widen or narrow it. |
reorderableColumns | boolean | false | Whether a reader may drag a header to put the column somewhere else. |
columnWidths | Record<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. |
minColumnWidth | number | 48 | The narrowest a column may be dragged. |
virtual | boolean | false | 只画视口覆盖到的行,并为其余的行预留高度,适合成千上万行的表。默认关闭,而且有代价:视野之外的行不在文档里,浏览器自带的查找和打印只能触及画出来的部分,且各行高度必须一致。开启时还会给表 24rem 的高度,因为任其生长的表和内容一样高,那样就没有东西可虚拟化了。 |
rowHeight | number | 0 | 虚拟化时一行的高度,单位像素。0 表示量第一行画出来的行;只要各行相仿就没问题。表若是在隐藏状态下搭起来的,请明确写出:从未布局过的行量出来是 0,而从 0 推出的窗口就是整张表。 |
formulas | boolean | false | 值以 = 开头的单元格算不算公式。不主动开启:文本列完全可能就把 =1+1 当作某人写下的内容。公式可以用别名指代列——=[units] * [price]、=SUM([total:])——挑动列或改写表头之后意思依旧;也可以用表格软件那种坐标——=ROUND(SUM(E1:E4), 2)——坐标说的是列“在哪里”而不是“是哪一列”,所以 rewriteRowFormulas() 会让它们随表格的形状一起移动,而指向已删除内容的引用会变成 #REF!,而不是默默读旁边一列。冒号读取整列并排除发问的那个单元格;其他的环会显示为 #CYCLE!,而不是一路追下去。 |
mergeable | boolean | false | 读者能否合并单元格并再拆开,通过右键菜单提供,所以也需要 contextMenu。和所有改变结构的功能一样,不主动开启。 |
editOn | 'click' | 'double-click' | 'double-click' | 用指针打开编辑器的方式。默认是双击,即表格的惯例:单击只选中单元格,不动它的值;键入的第一个字符会打开编辑器并落在里面;带列表或日期的单元格会显示一个标记,单击它即可打开。设为 click 则第一次单击就打开,适合本来就是用来录入的表。两种方式下编辑器都在松开时打开,且仅在指针没有移动时,所以拖出区域照常可用。 |
contextMenu | boolean | false | Whether 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[].formula | string | — | 本列每个单元格据以算出的公式,写在代码里而不是存在数据中:行小计、税额、滚动余额。与读者敲入的公式正好相反,那种是行持有的值,可以像别的值一样改。这种列的单元格根本打不开:替换掉一个会只在那一行丢掉公式,而这要等到合计对不上才会被发现。需要表上的 formulas。 |
columns[].placeholder | string | — | 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[].options | readonly 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. |
structure | HubSpreadsheetStructureOptions<TRow> | {} | Which structural changes the reader may ask for. Everything is refused until allowed. |
retiredColumnKeys | readonly string[] | [] | Aliases of columns that were deleted, so a new one never inherits a dead identity. |
输出
响应 spreadsheet 的行为——有 12 个事件可供你挂接逻辑。
| 名称 | 类型 | 描述 |
|---|---|---|
commit | HubSpreadsheetCommit<TRow> | A cell took a new value, and it differs from the one it held. |
pasted | HubSpreadsheetPaste<TRow> | A block was pasted, with counts of what fell outside the sheet and what was not a number. |
cleared | HubSpreadsheetCellRef<TRow>[] | Delete was pressed over a selection; carries the editable cells it covered. |
selectionChange | HubGridRange | null | The selected rectangle changed. |
filled | HubSpreadsheetPaste<TRow> | A drag of the fill handle finished, reported as one event for the whole gesture. |
undoRequested | void | The reader asked to undo. |
redoRequested | void | The 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. |
mergeRequested | HubSpreadsheetMergeRequest | 读者要求合并一片选区。负载里有要创建的合并块,以及它吞掉的那些块的锚点:只追加而不移除它们,就会有两个块争同一个单元格。applySpanMerge() 一行搞定。 |
unmergeRequested | readonly HubGridCoords[] | 读者要求拆开选区碰到的合并块,以锚点指明。用 applySpanUnmerge() 应用。 |
insertRequested | HubSpreadsheetInsertRequest | The reader asked to add rows or columns. |
deleteRequested | HubSpreadsheetDeleteRequest | The reader asked to remove rows or columns, naming the aliases at stake. |
方法
通过代码驱动 spreadsheet——其编程接口提供 17 个方法。
| 名称 | 签名 | 返回值 | 描述 |
|---|---|---|---|
sheetToXlsx | sheetToXlsx<TRow>(rows, columns, options?: HubXlsxOptions): Uint8Array | Uint8Array | 把表格写成真正的 .xlsx,返回字节。数字仍是数字,日期列就是日期,这是 CSV 装不下的。写出的是单元格显示的内容:列表列导出它的标签,公式导出它的结果;要行里存的原值就传 values: 'stored'。 |
downloadXlsx | downloadXlsx(filename: string, bytes: Uint8Array): void | — | 把那个工作簿交给读者保存,名字里缺 .xlsx 就补上。放在这里是因为每个宿主都要写同样的六行,而且都忘了释放 object URL。 |
xlsxToRecords | xlsxToRecords<TRow>(bytes, columns, options?: HubXlsxReadOptions): Promise<Array<Record<string, string>>> | Promise<Array<Record<string, string>>> | 把工作簿的第一张表读成以列别名为键的行,并还原 CSV 里根本不存在的两件事:共享字符串,以及以天数保存的日期。它是异步的,因为真正的工作簿是压缩的,解压用的是平台自带的能力。 |
sheetToCsv | sheetToCsv<TRow>(rows, columns, options?: HubCsvOptions): string | string | 把表格写成 CSV。不指定分隔符时由小数点符号决定——逗号不能既当小数点又当分隔符——所以给写 1.234,56 的人看的文件用分号。 |
downloadText | downloadText(filename: string, text: string, type?: string): void | — | 把一个文本文件交给读者保存,带上让表格软件按 UTF-8 打开的字节序标记,不会变成乱码。 |
csvToRecords | csvToRecords<TRow>(text, columns, options?: HubCsvOptions): Array<Record<string, string>> | Array<Record<string, string>> | 把 CSV 读成以列别名为键的行,分隔符是从第一行嗅出来的,不是猜的。表头先按表头、再按别名匹配,忽略大小写和两端空格。 |
sheetValues | sheetValues<TRow>(rows, columns, options?: HubSheetValuesOptions): HubSpreadsheetValue[][] | HubSpreadsheetValue[][] | 每个单元格的内容,公式已经算好,算不出来的按表格软件的写法写。两个导出读的都是它,公开出来给要自己写文件的宿主用。 |
evaluateSheet | evaluateSheet<TRow>(input: HubFormulaSheetInput<TRow>): HubFormulaSheet | HubFormulaSheet | 按依赖要求的顺序跑完表里所有公式,用到才算,算过就记住。环一闭合就被抓住,不会一路追下去。公开出来给只要结果、不需要绘制的宿主用。 |
rewriteRowFormulas | rewriteRowFormulas<TRow extends object>(rows, change: HubFormulaShapeChange): TRow[] | TRow[] | 表格变形之后——列挪了位置,或者插入、删除了行列——把行里所有坐标都挪过去。指向已删除内容的引用会变成 #REF!,而不是默默去读旁边一列。由宿主调用,因为行是宿主的。 |
mergeRequestFor | mergeRequestFor(spans, range): HubSpreadsheetMergeRequest | null | HubSpreadsheetMergeRequest | null | 一个选区会变成的合并块,以及它吞掉的锚点;只有一个单元格时返回 null。被吞掉的锚点正是宿主自己容易做错的地方:加了块却不去掉它们,两个块就会争同一个单元格。 |
applySpanMerge | applySpanMerge(spans, request: HubSpreadsheetMergeRequest): HubGridSpan[] | HubGridSpan[] | 这次合并之后的块列表,被吞掉的已经去掉。 |
applySpanUnmerge | applySpanUnmerge(spans, anchors: readonly HubGridCoords[]): HubGridSpan[] | HubGridSpan[] | 把那些块拆开之后的列表。 |
spreadsheetRecords | spreadsheetRecords<TRow>(rows, columns): HubSpreadsheetRecord[] | HubSpreadsheetRecord[] | 每一行都是以列别名为键的纯数据,于是可以直接用 { price: 12, units: 3 },而不用按位置把坐标对到自己的字段上。 |
parseForColumn | parseForColumn<TRow>(text, column, decimalMark: ',' | '.'): HubSpreadsheetValue | undefined | HubSpreadsheetValue | undefined | 一段敲进去或粘进来的文字在某一列里值多少:值本身,空单元格是 null,这一列装不了的东西是 undefined——于是调用方可以拒绝,而不是把垃圾存下来。 |
nextColumnKey | nextColumnKey(taken: Iterable<string>, prefix?: string): string | string | 一个从来不属于别的列的别名。新列不会重用退役的名字,任何已保存的引用也就不可能指到别处去。 |
danglingColumnKeys | danglingColumnKeys<TRow>(referenced: Iterable<string>, columns): string[] | string[] | 已经解析不到任何列的引用,也就是一次删除会留下的悬空指向。 |
provideHubSpreadsheetControls | provideHubSpreadsheetControls(adapter: HubSpreadsheetControlsAdapter): EnvironmentProviders | EnvironmentProviders | 注册一个控件适配器,之后每个列表列都用它打开——带搜索、已经展开,打开单元格的那个字符也落在搜索框里。没有适配器时表格画自己的原生列表,这正是这个库能单独安装的原因。 |
模板
打造你自己的样式——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> |