Summary
A plugin block can capture a list of rows, but not a table. repeater is the
closest element, and its row shape is fixed by the plugin at declaration time,
so a block built around it models one particular table rather than tables. A
plugin that wants to hand editors a designed data table therefore has to ship
the table as opaque HTML, which is exactly the content a block API is meant to
open up.
Verified against @emdash-cms/blocks@0.32.0 and @emdash-cms/admin@0.32.0,
re-checked unchanged in 0.33.0 (current latest on npm).
Where
Block Kit has a table, but it is a block, not an element: read-only
rows with plugin-declared columns and a page_action_id for pagination,
intended for plugin admin pages.
interface TableColumn { key: string; label: string; format?: …; sortable?: boolean }
// TableBlock: { columns: TableColumn[]; rows: Array<Record<string, unknown>>; page_action_id: string; … }
A Portable Text block's editing modal renders fields, which are elements,
and the element list in @emdash-cms/blocks src/validation.ts is:
button, text_input, number_input, select, toggle, secret_input,
checkbox, radio, date_input, combobox, repeater, media_picker
Nothing there is tabular. (BlockKitField in the admin renders six of those
twelve; the rest fall to Unknown field type. Noted in #2307, not this issue.)
Why repeater is not the answer
repeater gives the editor control of the rows and the plugin control of
the columns. Declaring four text_input sub-fields produces a four-column
table forever: an editor with a three-column table gets a stray empty column
they cannot remove, and an editor with five has nowhere to put the fifth. The
column count is content, not schema, and the block cannot express that.
Why the editor's own table node is not the answer either
The prose editor has a Table node, so an editor can insert a table into the
surrounding prose. But it lives outside the plugin block, so the plugin has no
say in how it renders and gets none of its data. A designed table that carries
plugin-owned classes, a caption convention or a render component cannot be built
on it. (It also has open limits of its own: #2242, #2336.)
Reproducing
- Register a Portable Text block with a
repeater of four text_input
sub-fields, intending it as a data table.
- Insert it and fill three columns. The fourth is empty in every row and there
is no way to remove it.
- There is no declaration that lets the editor add a fifth.
Suggested shape
A table_input element whose columns are editor-managed, with headers as
values rather than as schema:
{ type: "table_input", action_id: "data", label: "Table",
min_columns?: number, max_columns?: number,
min_rows?: number, max_rows?: number,
column_label?: string } // e.g. "Column" → "Add Column"
storing something a renderer can consume directly:
{ "columns": ["Metric", "Before", "After"],
"rows": [["Time to first draft", "3 days", "2 hours"]] }
That reuses the repeater's interaction model (add, remove, reorder) on a second
axis, and it keeps the plugin in charge of rendering while leaving the shape of
the data to the editor.
If a full element is too much, a narrower version would still unblock most of
this: let a repeater declare that its sub-field set is editor-extensible, so
columns can be added at authoring time from a single declared sub-field type.
Happy to send a PR if you would like a particular shape.
Summary
A plugin block can capture a list of rows, but not a table.
repeateris theclosest element, and its row shape is fixed by the plugin at declaration time,
so a block built around it models one particular table rather than tables. A
plugin that wants to hand editors a designed data table therefore has to ship
the table as opaque HTML, which is exactly the content a block API is meant to
open up.
Verified against
@emdash-cms/blocks@0.32.0and@emdash-cms/admin@0.32.0,re-checked unchanged in
0.33.0(currentlateston npm).Where
Block Kit has a
table, but it is a block, not an element: read-onlyrows with plugin-declared columns and a
page_action_idfor pagination,intended for plugin admin pages.
A Portable Text block's editing modal renders
fields, which are elements,and the element list in
@emdash-cms/blockssrc/validation.tsis:Nothing there is tabular. (
BlockKitFieldin the admin renders six of thosetwelve; the rest fall to
Unknown field type. Noted in #2307, not this issue.)Why
repeateris not the answerrepeatergives the editor control of the rows and the plugin control ofthe columns. Declaring four
text_inputsub-fields produces a four-columntable forever: an editor with a three-column table gets a stray empty column
they cannot remove, and an editor with five has nowhere to put the fifth. The
column count is content, not schema, and the block cannot express that.
Why the editor's own table node is not the answer either
The prose editor has a Table node, so an editor can insert a table into the
surrounding prose. But it lives outside the plugin block, so the plugin has no
say in how it renders and gets none of its data. A designed table that carries
plugin-owned classes, a caption convention or a render component cannot be built
on it. (It also has open limits of its own: #2242, #2336.)
Reproducing
repeaterof fourtext_inputsub-fields, intending it as a data table.
is no way to remove it.
Suggested shape
A
table_inputelement whose columns are editor-managed, with headers asvalues rather than as schema:
storing something a renderer can consume directly:
{ "columns": ["Metric", "Before", "After"], "rows": [["Time to first draft", "3 days", "2 hours"]] }That reuses the repeater's interaction model (add, remove, reorder) on a second
axis, and it keeps the plugin in charge of rendering while leaving the shape of
the data to the editor.
If a full element is too much, a narrower version would still unblock most of
this: let a
repeaterdeclare that its sub-field set is editor-extensible, socolumns can be added at authoring time from a single declared sub-field type.
Happy to send a PR if you would like a particular shape.