Summary
RepeaterElement is the only Block Kit element that captures more than one
value, and its sub-fields are restricted to four scalar element types. A
content shape as ordinary as "a list of cases, each with a title and three
labelled rows" therefore has no typed representation: the block either flattens
to a fixed number of scalar sub-fields, or the whole region stays an opaque HTML
atom that the editor cannot touch.
Verified against @emdash-cms/blocks@0.32.0, and re-checked unchanged in
0.33.0 (current latest on npm).
Where
The union, in @emdash-cms/blocks src/types.ts:
/**
* Sub-field types allowed inside a RepeaterElement. Limited to the scalar
* inputs the admin widget currently renders inline.
*/
type RepeaterSubField = TextInputElement | NumberInputElement | SelectElement | ToggleElement;
It is enforced at runtime too, not only by types, in src/validation.ts:
const REPEATER_SUB_FIELD_TYPES = new Set(["text_input", "number_input", "select", "toggle"]);
message: `Repeater sub-field type '${sub.type}' is not allowed. Expected one of: …`
Note that media_picker is a valid top-level element and is also excluded from
that set, so a repeater of images is out of reach by the same rule. (#1424
added media support to the content-type repeater field; the Block Kit
repeater is a separate surface and did not change.)
Reproducing
elements.repeater("cases", "Cases", [
elements.textInput("title", "Title"),
// Rejected by the type, and by validateBlocks at runtime:
elements.repeater("rows", "Rows", [
elements.textInput("label", "Label"),
elements.textInput("value", "Value"),
]),
])
TypeScript rejects the nested element, and forcing it past the types produces
Repeater sub-field type 'repeater' is not allowed.
The interesting part: the renderer may already be closer than the types
In @emdash-cms/admin src/components/PortableTextEditor.tsx, a repeater row
renders each of its sub-fields through BlockKitField — the same component
used at the top level of the modal, whose switch already has a
case "repeater" that delegates to BlockKitRepeater. So the row renderer
would dispatch a nested repeater today.
Two things in the same file would need attention before that worked:
handleAdd seeds a new row from the sub-field type, with false for
toggle, undefined for number_input, and "" for everything else. A
nested repeater would be seeded with a string rather than [].
stripKeys removes _key at one level only, so nested rows would keep the
synthetic keys the widget adds.
That is a reading of the source, not a test result — I did not force a nested
repeater through to see what the widget does with it.
Why it matters
The alternative shapes are worse in ways that show up in the editor:
- Flattening to
row1_label, row1_value, row2_label … fixes the count
at declaration time and produces a modal of eight or more unrelated inputs
with no visual grouping. It also silently caps the content.
- Leaving it as HTML keeps the region editable only by a developer, which is
the outcome the block API exists to avoid.
The shape is common: FAQ groups, comparison cases, timelines with sub-steps,
spec sheets. One level of nesting covers all of them.
Suggested fix
- Allow
RepeaterElement in RepeaterSubField, bounded to one level of
nesting so the shape stays predictable and the widget stays simple. Add
"repeater" to REPEATER_SUB_FIELD_TYPES, seed nested rows as [] in
handleAdd, and recurse in stripKeys.
- If nesting is deliberately out of scope, say so in the
RepeaterSubField
docstring. The current wording ("Limited to the scalar inputs the admin
widget currently renders inline") reads as a temporary implementation limit,
so plugin authors reasonably try it and hit the runtime error.
Happy to send a PR for (1) if you would like a particular shape.
Summary
RepeaterElementis the only Block Kit element that captures more than onevalue, and its sub-fields are restricted to four scalar element types. A
content shape as ordinary as "a list of cases, each with a title and three
labelled rows" therefore has no typed representation: the block either flattens
to a fixed number of scalar sub-fields, or the whole region stays an opaque HTML
atom that the editor cannot touch.
Verified against
@emdash-cms/blocks@0.32.0, and re-checked unchanged in0.33.0(currentlateston npm).Where
The union, in
@emdash-cms/blockssrc/types.ts:It is enforced at runtime too, not only by types, in
src/validation.ts:message: `Repeater sub-field type '${sub.type}' is not allowed. Expected one of: …`Note that
media_pickeris a valid top-level element and is also excluded fromthat set, so a repeater of images is out of reach by the same rule. (#1424
added media support to the content-type repeater field; the Block Kit
repeater is a separate surface and did not change.)
Reproducing
TypeScript rejects the nested element, and forcing it past the types produces
Repeater sub-field type 'repeater' is not allowed.The interesting part: the renderer may already be closer than the types
In
@emdash-cms/adminsrc/components/PortableTextEditor.tsx, a repeater rowrenders each of its sub-fields through
BlockKitField— the same componentused at the top level of the modal, whose switch already has a
case "repeater"that delegates toBlockKitRepeater. So the row rendererwould dispatch a nested repeater today.
Two things in the same file would need attention before that worked:
handleAddseeds a new row from the sub-field type, withfalsefortoggle,undefinedfornumber_input, and""for everything else. Anested repeater would be seeded with a string rather than
[].stripKeysremoves_keyat one level only, so nested rows would keep thesynthetic keys the widget adds.
That is a reading of the source, not a test result — I did not force a nested
repeater through to see what the widget does with it.
Why it matters
The alternative shapes are worse in ways that show up in the editor:
row1_label,row1_value,row2_label… fixes the countat declaration time and produces a modal of eight or more unrelated inputs
with no visual grouping. It also silently caps the content.
the outcome the block API exists to avoid.
The shape is common: FAQ groups, comparison cases, timelines with sub-steps,
spec sheets. One level of nesting covers all of them.
Suggested fix
RepeaterElementinRepeaterSubField, bounded to one level ofnesting so the shape stays predictable and the widget stays simple. Add
"repeater"toREPEATER_SUB_FIELD_TYPES, seed nested rows as[]inhandleAdd, and recurse instripKeys.RepeaterSubFielddocstring. The current wording ("Limited to the scalar inputs the admin
widget currently renders inline") reads as a temporary implementation limit,
so plugin authors reasonably try it and hit the runtime error.
Happy to send a PR for (1) if you would like a particular shape.