Getting Started
Overview
Installation
Theme
Forms
Basic Inputs
Custom Inputs
Select
Components
Components
78
Micro Charts
Navigation
Libraries
Content Editor
Data View
Form Builder
Headless Editor
Image Designer
Kanban Board
PDF Builder
PDF Signer
PDF Viewer
Video Player
GitHub repositoryGitHub starsGitHub forksNgStarter UI npm versionNgStarter UI npm downloads per month

Tables

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.

Loading example…

Enable tables

import {
  basicTextEditorPlugin,
  provideNgsHeadlessEditor,
  tableEditorPlugin,
  withHeadlessEditorPlugin
} from '@ngstarter-ui/components/headless-editor';

@Component({
  providers: [
    provideNgsHeadlessEditor(
      withHeadlessEditorPlugin(basicTextEditorPlugin()),
      withHeadlessEditorPlugin(tableEditorPlugin())
    )
  ]
})
export class DocumentEditor {}

Data model

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: [] }]
      ]
    ]
  }
}

Inserting tables

NGS_HEADLESS_EDITOR_INSERT_TABLE takes an optional size; without one it inserts a 3 × 3 table with a header row.

  • An empty paragraph at the caret is replaced by the table; otherwise the table goes after the current block.
  • When the table would be the last block, an empty paragraph is added after it, so you can keep writing below.
  • Focus moves to the first cell. The insertion is one undo step.
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%']
]));

Editing cells

KeyAction
click or focus a cellOpens the cell in the cell editor, with the caret at the click position.
typingEdits the cell. Consecutive typing in one cell is one undo step.
formatting shortcutsMod-b, Mod-i, and every other key binding of the editor's mark plugins.
EnterInserts a line break inside the cell.
Tab, Shift+TabMoves to the next or previous cell.
Tab in the last cellAppends a row and moves into it.
EscapeLeaves the cell editor and keeps focus on the cell.
Mod-z, Mod-Shift-zUndo and redo in the document history, the same as outside the table.
paste in a cellInserts the clipboard as plain text.

Formatting in cells

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.
  • A list of mark types allows only those marks in cells.
  • A mark definition with 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.

Commands

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.

CommandAction
NGS_HEADLESS_EDITOR_INSERT_TABLEInserts a table. Payload: rows, columns, header.
NGS_HEADLESS_EDITOR_TABLE_ADD_ROW_BEFOREAdds a row above the cell.
NGS_HEADLESS_EDITOR_TABLE_ADD_ROW_AFTERAdds a row below the cell.
NGS_HEADLESS_EDITOR_TABLE_ADD_COLUMN_BEFOREAdds a column to the left.
NGS_HEADLESS_EDITOR_TABLE_ADD_COLUMN_AFTERAdds a column to the right.
NGS_HEADLESS_EDITOR_TABLE_DELETE_ROWDeletes the row; disabled for the last row.
NGS_HEADLESS_EDITOR_TABLE_DELETE_COLUMNDeletes the column; disabled for the last column.
NGS_HEADLESS_EDITOR_TABLE_TOGGLE_HEADERTurns the header row on or off; active while it is on.
NGS_HEADLESS_EDITOR_TABLE_DELETERemoves 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 from spreadsheets

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.

  • Text becomes a table only when every line has the same number of cells, at least two, so tab-indented code stays text.
  • HTML becomes a table only when the clipboard contains nothing but the table, so copying an article that includes a table pastes text.
  • Formatting such as bold, italic, and links is kept for marks that are allowed in cells; other markup is discarded.
  • Disable it with tableEditorPlugin({ paste: false }).

Changing tables from code

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

Styling

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;
  }
}

Read-only rendering

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.

Limitations

  • A selection cannot start in text and end inside a table cell; the table is selected and deleted as one block.
  • Backspace at the start of the paragraph after a table removes the whole table, like any atomic block. Undo restores it.
  • Merged cells are not supported. Tables are limited to 500 rows and 50 columns.