Advanced components
This page covers optional Component extensions: getSlots(), separate, removeSlot, deleteAsWhole, and static zenCoding. Together with Commander (delete, paste, transform), Selection, and event hooks they define how multi-slot blocks behave while editing.
Read first: Component basics, Component events & lifecycle, Query & operations, Shortcuts & grammar.
getSlots(): Slot[]
Role: list every child Slot on this component instance in document model order—the order must match top-to-bottom, front-to-back rendering in the view.
Returns: Slot[]. The slots accessor on the instance calls getSlots() internally; selection math, Commander.delete at block boundaries, paste, and transform all read child slots in slots order.
Convention: if you implement removeSlot so it can return true, or removeSlot / separate cause the kernel to splice the slots array, getSlots() should keep returning the same array reference (e.g. hold Slot[] on state and return it from getSlots()). Otherwise splice hits a temporary array and state drifts from what the kernel sees.
import { Component, ContentType, Slot } from '@textbus/core'
type RowState = { cells: Slot[] }
abstract class RowLike extends Component<RowState> {
override getSlots(): Slot[] {
return this.state.cells
}
}separate(start?, end?): Component
Role: cut a **contiguous range of child Slot**s from this component into a new instance of the same class; after split, this instance no longer owns those slots—you move state / references in your implementation.
Parameters ( Slot references, not numeric indices):
start: first childSlotto peel off (inclusive).end: last childSlot(inclusive). If omitted, meaning depends on theCommandercall site (often same asstartor single-slot split).
Returns: new Component instance of the same type; the kernel **insertAfter**s it following the original block. paste and transform use this when trailing slots must become a sibling block (see When it runs below).
When it runs:
paste: when pasted content cannotinsertdirectly into the selection, if the parent implementsseparate, the kernel takesnextSlotafter the selection’s slot and callsparentComponent.separate(nextSlot)to peel trailing structure into a new block, then continues insertion.transform: when a multi-slot parent must promote trailing child slots to a sibling, it **splicesparentComponent.slots, thenseparate(deletedSlots[0], deletedSlots[deletedSlots.length - 1])andinsertAfterthe returned component.
Without separate, those paths often degrade to partial insert or odd leftover structure; implement it—aligned with getSlots()—when you need new peers from the middle or tail (lists, table rows, …).
import { Component, ContentType, Slot } from '@textbus/core'
type GridState = { rows: Slot[] }
declare class GridRow extends Component<GridState> {
static componentName = 'GridRow'
static type = ContentType.BlockComponent
}
// Illustrative: peel cells from slot `start` through `end` into a new row
function exampleSeparate(row: GridRow, start: Slot, end: Slot) {
const idx = row.state.rows.indexOf(start)
const endIdx = row.state.rows.indexOf(end)
const moved = row.state.rows.splice(idx, endIdx - idx + 1)
return new GridRow({ rows: moved })
}(GridRow / state are placeholders—match fromJSON, view, schema in real code.)
removeSlot(slot): boolean
Role: when Commander.delete with a collapsed caret deletes backward from a non-first child slot and must remove the whole child slot, the kernel asks parentComponent.removeSlot(slot) first.
Parameter: slot — the Slot about to leave the parent’s child-slot list.
Returns:
true: you removed it (updatedstate, dropped references, …). The kernel then **splicesparentComponent.slots(fromgetSlots()) to drop that slot entry.falseor missing implementation: kernel does not mutatestate; delete falls back to default (caret may stay on the oldSlot).
So true must match getSlots()’s backing array—or you get “kernel **splice**d but state still holds the old Slot.”
import { Component, ContentType, Slot } from '@textbus/core'
type RowState = { cells: Slot[] }
class TableRow extends Component<RowState> {
static componentName = 'TableRow'
static type = ContentType.BlockComponent
getSlots(): Slot[] {
return this.state.cells
}
removeSlot(slot: Slot): boolean {
const i = this.state.cells.indexOf(slot)
if (i <= 0) {
return false
}
this.state.cells.splice(i, 1)
return true
}
}deleteAsWhole?: boolean
Role: optional boolean field on the instance (not a method). When Commander.delete with collapsed selection has a Component adjacent on one side:
- If that component
type === BlockComponentordeleteAsWhole === true: deleteremoveComponents the whole block—caret does not enter the block first. - Otherwise: default “drill into children” rules apply.
false or omit: inline blocks or blocks where caret should enter—keep default.
import { Component, ContentType, Slot } from '@textbus/core'
class Card extends Component<{ body: Slot }> {
static componentName = 'Card'
static type = ContentType.BlockComponent
constructor(init: { body: Slot }) {
super(init)
this.deleteAsWhole = true
}
}Static zenCoding
static zenCoding on the class, plus TextbusConfig.zenCoding and keyboard.addZenCodingInterceptor, implement Zen Coding. match / key / createState, timing, parent Slot.schema, and the runnable Todolist sample live in Shortcuts & grammar under “Static zenCoding on component classes”—not duplicated here.
Relation to Commander and events
transform,paste,deleteinvokeseparate/removeSlot/deleteAsWholefrom selection and common ancestor—parameters and failure modes: Query & operations.onPaste,onBreak,onContentDelete, … apply to the same document tree; order andpreventDefault: Component events & lifecycle.
