

tableEditorPlugin() adds a table block. Tables are component blocks: the grid is stored in the block attributes and edited by a built-in Angular component, with commands for rows, columns, and the header row, and pasting of spreadsheet ranges. Cells hold rich text: the formatting toolbar and shortcuts of the editor work inside a focused cell.
import {
basicTextEditorPlugin,
provideNgsHeadlessEditor,
tableEditorPlugin,
withHeadlessEditorPlugin
} from '@ngstarter-ui/components/headless-editor';
@Component({
providers: [
provideNgsHeadlessEditor(
withHeadlessEditorPlugin(basicTextEditorPlugin()),
withHeadlessEditorPlugin(tableEditorPlugin())
)
]
})
export class DocumentEditor {} A table is an atomic block. Its attrs hold the cells by row and column and whether the first row is a header. Each cell is a list of text runs with marks, like a paragraph; a line break inside a cell is a newline character. Plain strings are accepted wherever cells are created and are converted to runs. Rows are always rectangular: missing cells are filled with empty cells when the block is read.
{
id: 'table-1',
type: 'table',
content: null,
attrs: {
header: true,
rows: [
[
[{ type: 'text', text: 'Plan', marks: [] }],
[{ type: 'text', text: 'Price', marks: [] }]
],
[
[{ type: 'text', text: 'Team', marks: [{ type: 'bold' }] }],
[{ type: 'text', text: '$49\nper month', marks: [] }]
]
]
}
}NGS_HEADLESS_EDITOR_INSERT_TABLE takes an optional size; without one it inserts a 3 × 3 table with a header row.
editor.execute(NGS_HEADLESS_EDITOR_INSERT_TABLE); // 3 x 3, header
editor.execute(NGS_HEADLESS_EDITOR_INSERT_TABLE, { rows: 4, columns: 2, header: false });
// Or build the block yourself:
insertNgsHeadlessEditorTable(editor, createNgsHeadlessEditorTable([
['Metric', 'Value'],
['Uptime', '99.98%']
]));| Key | Action |
|---|---|
| click or focus a cell | Opens the cell in the cell editor, with the caret at the click position. |
| typing | Edits the cell. Consecutive typing in one cell is one undo step. |
| formatting shortcuts | Mod-b, Mod-i, and every other key binding of the editor's mark plugins. |
| Enter | Inserts a line break inside the cell. |
| Tab, Shift+Tab | Moves to the next or previous cell. |
| Tab in the last cell | Appends a row and moves into it. |
| Escape | Leaves the cell editor and keeps focus on the cell. |
| Mod-z, Mod-Shift-z | Undo and redo in the document history, the same as outside the table. |
| paste in a cell | Inserts the clipboard as plain text. |
While a cell is being edited it is the editor's inline target: formatting commands, mark state, stored marks, and insertText() of the document editor act on the cell. Your toolbar does not change; a Bold button bound with ngsHeadlessEditorCommand shows and toggles the state of the cell. Block commands such as headings are disabled inside a cell.
Every mark of the editor is available in cells unless it is switched off explicitly:
withHeadlessEditorPlugin(tableEditorPlugin()) // all marks
withHeadlessEditorPlugin(tableEditorPlugin({ formatting: ['bold', 'italic', 'link'] })) // some marks
withHeadlessEditorPlugin(tableEditorPlugin({ formatting: false })) // plain text
// A mark that never appears in nested editors:
{ type: 'comment', tagName: 'mark', nested: false }formatting: false keeps cells plain text; formatting commands are disabled in cells.nested: false is never available in cells or other nested editors. Cells use the mark definitions of your plugins, so custom marks render and behave the same inside and outside tables. Commands should use enabled: editor => editor.canApplyMark(type) so buttons disable themselves where a mark is not allowed; see Marks and Formatting.
Table commands act on the focused cell, or the last focused one while a toolbar button is pressed. They are disabled when no table cell has focus, so toolbars bound with ngsHeadlessEditorCommand enable themselves automatically.
| Command | Action |
|---|---|
NGS_HEADLESS_EDITOR_INSERT_TABLE | Inserts a table. Payload: rows, columns, header. |
NGS_HEADLESS_EDITOR_TABLE_ADD_ROW_BEFORE | Adds a row above the cell. |
NGS_HEADLESS_EDITOR_TABLE_ADD_ROW_AFTER | Adds a row below the cell. |
NGS_HEADLESS_EDITOR_TABLE_ADD_COLUMN_BEFORE | Adds a column to the left. |
NGS_HEADLESS_EDITOR_TABLE_ADD_COLUMN_AFTER | Adds a column to the right. |
NGS_HEADLESS_EDITOR_TABLE_DELETE_ROW | Deletes the row; disabled for the last row. |
NGS_HEADLESS_EDITOR_TABLE_DELETE_COLUMN | Deletes the column; disabled for the last column. |
NGS_HEADLESS_EDITOR_TABLE_TOGGLE_HEADER | Turns the header row on or off; active while it is on. |
NGS_HEADLESS_EDITOR_TABLE_DELETE | Removes the table. |
<button ngsButton="outlined" [ngsHeadlessEditorCommand]="insertTable" [commandData]="{ rows: 3, columns: 3, header: true }">
Insert table
</button>
<button ngsButton="text" [ngsHeadlessEditorCommand]="rowAfter">Row below</button>
<button ngsButton="text" [ngsHeadlessEditorCommand]="columnAfter">Column right</button>
<button ngsButton="text" [ngsHeadlessEditorCommand]="toggleHeader">Header row</button>
<!--
readonly insertTable = NGS_HEADLESS_EDITOR_INSERT_TABLE;
readonly rowAfter = NGS_HEADLESS_EDITOR_TABLE_ADD_ROW_AFTER;
readonly columnAfter = NGS_HEADLESS_EDITOR_TABLE_ADD_COLUMN_AFTER;
readonly toggleHeader = NGS_HEADLESS_EDITOR_TABLE_TOGGLE_HEADER;
-->ngsHeadlessEditorActiveTableCell(editor) returns a signal with the active cell, for example to show contextual table controls. focusNgsHeadlessEditorTableCell(editor, cell) moves focus to a cell once its table is rendered.
Pasting a range copied from Excel, Google Sheets, or Numbers into the editor text creates a table. Both the tab-separated text and the HTML table of the clipboard are recognized.
tableEditorPlugin({ paste: false }).Table data is plain JSON, so it can be generated, validated, and changed with ordinary functions. The pure helpers never mutate their input.
const block = editor.document().blocks.find(item => item.type === 'table')!;
let data = getNgsHeadlessEditorTableData(block); // normalized cells + header
const price = getNgsHeadlessEditorTableCellText(data.rows[1][2]);
data = setNgsHeadlessEditorTableCell(data, 1, 2, '$9'); // plain text
data = setNgsHeadlessEditorTableCell(data, 0, 0, [
createNgsHeadlessEditorText('Plan', [{ type: 'bold' }]) // or text runs
]);
data = insertNgsHeadlessEditorTableRow(data, data.rows.length);
data = removeNgsHeadlessEditorTableColumn(data, 1);
editor.updateBlock(block.id, { attrs: { ...block.attrs, rows: data.rows, header: data.header } });
// Also available: insertNgsHeadlessEditorTableColumn, removeNgsHeadlessEditorTableRow,
// parseNgsHeadlessEditorTableText (TSV), parseNgsHeadlessEditorTableHtml The table components only set structural styles. Style them from the surface with ::ng-deep or global styles through these hooks:
.ngs-headless-editor-table on the table, with .has-header when the header row is on;.ngs-headless-editor-table-cell on every cell, th for header cells;.active on the focused cell while editing;.ngs-headless-editor-table-cell-content for the static content of a cell and .ngs-headless-editor-table-cell-editor for the editor of the focused cell..surface ::ng-deep {
.ngs-headless-editor-table {
width: 100%;
}
.ngs-headless-editor-table-cell {
padding: 0.5rem 0.75rem;
border: 1px solid var(--ngs-color-border);
outline: none;
&.active {
box-shadow: inset 0 0 0 2px var(--ngs-color-primary);
}
}
th.ngs-headless-editor-table-cell {
background: var(--ngs-color-surface-container-low);
font-weight: 600;
}
} While the surface is disabled or the editor is read-only, tables are rendered by a static component with a real thead for the header row.