BlockAPI: work with a single block
Interface for working with individual blocks. Returned by blocks.getById(), blocks.getBlockByIndex(), and blocks.insert().
Reaching the editor instance
The methods below run on the editor you created with new Blok(). They are available once editor.isReady resolves.
// You already hold the instance returned by the constructor.
const editor = new Blok({ holder: 'editor' });
await editor.isReady;
// Call any API method on it.
editor.caret.setToLastBlock('end');Methods
block.save()
Promise<void|SavedData>Save the block content and return its data.
When to use
Returns the data for this block only. Use it to inspect or persist one block without the full walk that editor.save() does.
const block = editor.blocks.getById('block-123');
const saved = await block.save();
// saved resolves to a SavedData object (or undefined if extraction fails):
// { id: 'block-123', tool: 'paragraph', data: { text: 'Block content' }, time: 1717000000000 }
console.log(saved?.data); // { text: 'Block content' }block.validate(data)
Promise<boolean>Validate block data against the tool's validation rules.
When to use
Runs the tool's validate() against the given data. Use it to reject empty or malformed blocks before saving.
const block = editor.blocks.getById('block-123');
const isValid = await block.validate({ text: 'Hello' });
if (!isValid) {
console.log('Block data is invalid');
}block.call(methodName, param?)
voidCall a custom method on the block's tool.
When to use
An escape hatch for calling a custom method your tool exposes. Use it for behaviour outside the standard BlockTool API.
const block = editor.blocks.getById('block-123');
// Call a custom method defined in the tool
block.call('showNotification', { message: 'Hello' });block.dispatchChange()
voidManually trigger the onChange callback for this block.
Pass { derived: true } when the change is data the tool worked out itself (a measured size, a fetched preview), not a user edit. It is saved but adds no undo step and keeps redo. Add from with the data keys it came from ({ derived: true, from: ['url'] }): it then joins the undo step that wrote those values.
When to use
Call it when you change a block outside Blok's knowledge (e.g. an async update), so change events and CRDT sync fire.
const block = editor.blocks.getById('block-123');
// Trigger change after invisible modification
block.dispatchChange();block.getActiveToolboxEntry()
Promise<ToolboxConfigEntry | undefined>Get the active toolbox entry for this block (e.g., Heading 1 vs Heading 2).
When to use
Resolves which toolbox variant is active (e.g. Heading 1 vs 2). Useful for reflecting state in custom UI.
const block = editor.blocks.getById('block-123');
const entry = await block.getActiveToolboxEntry();
if (entry) {
console.log('Active entry:', entry.title);
}block.getChildren()
BlockAPI[]This block's direct children as BlockAPI objects, in order. Every BlockAPI the editor hands out is live. So you can walk the children it returns recursively (child.getChildren()) and change them (child.setParent(...), child.insertChild(...)).
const block = editor.blocks.getById('toggle-123');
block?.getChildren().forEach((child) => console.log(child.id));block.setParent(parentId)
voidReparent this block under parentId, or back to the root level with null. It goes through core's single setBlockParent entry point, so the parent's contentIds is updated together with this block's parentId.
const block = editor.blocks.getById('block-123');
block?.setParent('parent-block-id');
// Back to the root level
block?.setParent(null);block.insertChild(childData?, position?, toolName?, options?)
BlockAPIInsert a child block under THIS block atomically. Creation and parent assignment land in a single undo entry (it delegates to blocks.insertInsideParent).
positionis aBlockChildPosition: 'start' | 'end' | { before: childId } | { after: childId }.- It defaults to 'end', which appends past the whole subtree.
toolNamepicks the child's block tool and defaults toconfig.defaultBlock, so a TYPED child is one operation instead of insert-then-reparent.- A tool that is restricted inside table cells is demoted to the default block when the new child would land inside one.
childDatadefaults to{ text: '' }for the default block, and to{}whentoolNamenames a different tool.optionsuses the same{ focus, caret, id, tunes, replace }vocabulary as the framework adapters' richinsertspec.
So a container tool never has to place the caret by hand, or follow up with an update to apply tunes.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
childData | BlockToolData | — | { text: '' } / {} | Data for the new child block. It defaults to an empty paragraph blob for the default block tool. When toolName is given it defaults to {}, which lets the tool apply its own defaults. |
position | BlockChildPosition | — | 'end' | Where among the existing children to insert: 'start', 'end', { before: childId } or { after: childId }. |
toolName | string | — | config.defaultBlock | Block tool to create for the child. It is demoted to the default block when it is restricted inside table cells and the new child would land inside one. |
options | InsertChildOptions | — | {} | focus makes the new child the current block. caret places the caret inside it ({ position?, offset? }), and applies only when a child is actually created. id sets an explicit id. An id that already exists works as insert-if-absent: that child is returned and nothing is created. tunes is block tune data applied at creation. replace overwrites the child named by an object position instead of inserting beside it. With 'start' or 'end' there is nothing to overwrite, so the call throws. |
const block = editor.blocks.getById('toggle-123');
const child = block?.insertChild({ text: 'Hidden content' });
// Place it among the existing children instead of appending
block?.insertChild({ text: 'First' }, 'start');
block?.insertChild({ text: 'After that one' }, { after: 'child-id' });
// A typed child, in a single undo entry
block?.insertChild({ text: 'Section', level: 3 }, 'end', 'header');
// Drop the caret into the new child at a specific offset
block?.insertChild({ text: 'Draft' }, 'end', undefined, { caret: { offset: 5 } });
// Idempotent: a re-running effect cannot duplicate this child
block?.insertChild({ text: 'Intro' }, 'start', undefined, { id: 'intro-row' });
// Child-level "turn into" — overwrite an existing child, keeping it parented
block?.insertChild({ text: 'Now a heading', level: 3 }, { before: 'child-id' }, 'header', { replace: true });block.moveChild(childId, delta)
voidMove a direct child by delta positions among its siblings, clamped to the valid range. A child carrying its own subtree lands past the target sibling's descendants, not inside them. It does nothing when delta is 0, when childId is not a direct child, or when the clamped move would not change the position.
const block = editor.blocks.getById('toggle-123');
block?.moveChild('child-id', -1); // one position toward the start
block?.moveChild('child-id', 1); // one position toward the endProperties
| Property | Type | Description |
|---|---|---|
id | string | Unique block identifier |
name | string | Tool name (e.g., "paragraph", "header") |
config | ToolConfig | Tool config passed on initialization |
holder | HTMLElement | Wrapper of Tool's HTML element |
isEmpty | boolean | True if block content is empty |
selected | boolean | True if the block is part of a BLOCK-level selection (rubber-band drag, Shift+Click, Shift+Arrow, Cmd/Ctrl+A). A drag across the text of several blocks makes a character-level selection instead. That marks no block as selected, so read it from the document's own Selection. |
focusable | boolean | True if block has inputs to be focused |
stretched | boolean | Getter/setter for block stretch state |
parentId | string | null | Id of the parent block, or null if this block has no parent |
contentIds | readonly string[] | Ids of this block's direct children, in order. It is a read-only copy, so changing it does nothing. This is the block-level counterpart of parentId: it lets a container tool read its children without reaching for the editor API |
preservedData | BlockToolData | Last successfully extracted block tool data. It is synchronous, so it helps when async save() is not feasible, e.g. clipboard operations. It is an internal snapshot, so rich text fields here stay HTML strings. |
preservedTunes | { [name: string]: BlockTuneData } | Last successfully extracted block tune data. It is synchronous, so it helps when async save() is not feasible, e.g. clipboard operations |