Manipulating Blocks
BlockNote provides a comprehensive set of APIs for reading, creating, updating, and removing blocks in the editor. These APIs allow you to programmatically manipulate the document structure and content.
Overview
The block manipulation APIs fall into several categories:
- Reading blocks - Accessing existing blocks and their relationships
- Creating blocks - Inserting new blocks into the document
- Updating blocks - Modifying existing block content and properties
- Removing blocks - Deleting blocks from the document
- Moving blocks - Reordering blocks within the document
- Nesting blocks - Creating hierarchical relationships between blocks
Common Types
Before diving into the APIs, let's understand the key types used throughout:
Block Identifiers
Most methods require a BlockIdentifier
to reference existing blocks:
type BlockIdentifier = string | Block;
You can pass either:
- A
string
representing the block ID - A
Block
object (the ID will be extracted automatically)
Partial Blocks
When creating or updating blocks, you use PartialBlock
objects which have optional properties:
type PartialBlock = {
id?: string; // Auto-generated if not provided
type?: string; // Block type (paragraph, heading, etc.)
props?: Partial<Record<string, any>>; // Block-specific properties
content?: string | InlineContent[] | TableContent; // Block content
children?: PartialBlock[]; // Nested blocks
};
This makes it easy to create simple blocks or update specific properties without specifying everything.
Reading Blocks
Getting the Document
Retrieve all top-level blocks in the editor:
const blocks = editor.document;
Returns a snapshot of all top-level (non-nested) blocks in the document.
Getting Specific Blocks
Single Block
getBlock(blockIdentifier: BlockIdentifier): Block | undefined
Retrieves a specific block by its identifier.
const block = editor.getBlock("block-123");
// or
const block = editor.getBlock(existingBlock);
Previous Block
getPrevBlock(blockIdentifier: BlockIdentifier): Block | undefined
Gets the previous sibling of a block.
const prevBlock = editor.getPrevBlock("block-123");
Next Block
getNextBlock(blockIdentifier: BlockIdentifier): Block | undefined
Gets the next sibling of a block.
const nextBlock = editor.getNextBlock("block-123");
Parent Block
getParentBlock(blockIdentifier: BlockIdentifier): Block | undefined
Gets the parent of a nested block.
const parentBlock = editor.getParentBlock("nested-block-123");
Traversing All Blocks
forEachBlock(
callback: (block: Block) => boolean | undefined,
reverse: boolean = false
): void
Traverses all blocks depth-first and executes a callback for each.
editor.forEachBlock((block) => {
console.log(`Block ${block.id}: ${block.type}`);
return true; // Continue traversal
});
Creating Blocks
Inserting Blocks
insertBlocks(
blocksToInsert: PartialBlock[],
referenceBlock: BlockIdentifier,
placement: "before" | "after" = "before"
): void
Inserts new blocks relative to an existing block.
// Insert a paragraph before an existing block
editor.insertBlocks(
[{ type: "paragraph", content: "New paragraph" }],
"existing-block-id",
"before",
);
// Insert multiple blocks after an existing block
editor.insertBlocks(
[
{ type: "heading", content: "New Section", props: { level: 2 } },
{ type: "paragraph", content: "Section content" },
],
"existing-block-id",
"after",
);
Updating Blocks
Modifying Existing Blocks
updateBlock(
blockToUpdate: BlockIdentifier,
update: PartialBlock
): void
Updates an existing block with new properties.
// Change a paragraph to a heading
editor.updateBlock("block-123", {
type: "heading",
props: { level: 2 },
});
// Update content only
editor.updateBlock("block-123", {
content: "Updated content",
});
// Update multiple properties
editor.updateBlock("block-123", {
type: "heading",
content: "New heading text",
props: { level: 1 },
});
Removing Blocks
Deleting Blocks
removeBlocks(blocksToRemove: BlockIdentifier[]): void
Removes one or more blocks from the document.
// Remove a single block
editor.removeBlocks(["block-123"]);
// Remove multiple blocks
editor.removeBlocks(["block-123", "block-456", "block-789"]);
Replacing Blocks
Swapping Blocks
replaceBlocks(
blocksToRemove: BlockIdentifier[],
blocksToInsert: PartialBlock[]
): void
Replaces existing blocks with new ones.
// Replace a paragraph with a heading
editor.replaceBlocks(
["paragraph-block"],
[{ type: "heading", content: "New Heading", props: { level: 2 } }],
);
// Replace multiple blocks with different content
editor.replaceBlocks(
["block-1", "block-2"],
[
{ type: "paragraph", content: "Replacement content" },
{ type: "bulletListItem", content: "List item" },
],
);
Moving Blocks
Reordering Blocks
moveBlocksUp(): void
moveBlocksDown(): void
Moves the currently selected blocks up or down in the document.
// Move selected blocks up
editor.moveBlocksUp();
// Move selected blocks down
editor.moveBlocksDown();
Nesting Blocks
Creating Hierarchical Structures
canNestBlock(): boolean
nestBlock(): void
canUnnestBlock(): boolean
unnestBlock(): void
Manages the nesting level of blocks (indentation).
// Check if current block can be nested
if (editor.canNestBlock()) {
editor.nestBlock(); // Indent the block
}
// Check if current block can be un-nested
if (editor.canUnnestBlock()) {
editor.unnestBlock(); // Outdent the block
}
Practical Examples
Now let's look at some real-world scenarios where these APIs are useful:
Example 1: Building a Table of Contents
function generateTableOfContents(editor: BlockNoteEditor) {
const toc: Array<{ level: number; text: string; id: string }> = [];
editor.forEachBlock((block) => {
if (block.type === "heading") {
toc.push({
level: block.props.level,
text: block.content as string,
id: block.id,
});
}
});
return toc;
}
// Usage
const toc = generateTableOfContents(editor);
console.log("Table of Contents:", toc);
Example 2: Converting Paragraphs to Headings
function convertParagraphsToHeadings(
editor: BlockNoteEditor,
level: number = 2,
) {
const blocks = editor.document;
blocks.forEach((block) => {
if (block.type === "paragraph" && block.content) {
const text = block.content as string;
// Convert paragraphs that start with # to headings
if (text.startsWith("#")) {
const headingText = text.replace(/^#+\s*/, "");
editor.updateBlock(block.id, {
type: "heading",
content: headingText,
props: { level },
});
}
}
});
}
Example 3: Duplicating Selected Blocks
function duplicateSelectedBlocks(editor: BlockNoteEditor) {
const selection = editor.getSelection();
if (selection && selection.blocks.length > 0) {
const blocksToInsert = selection.blocks.map((block) => ({
type: block.type,
content: block.content,
props: block.props,
children: block.children,
}));
// Insert duplicates after the last selected block
const lastBlock = selection.blocks[selection.blocks.length - 1];
editor.insertBlocks(blocksToInsert, lastBlock.id, "after");
}
}
Example 4: Creating a Template System
function insertTemplate(editor: BlockNoteEditor, templateName: string) {
const templates = {
meeting: [
{ type: "heading", content: "Meeting Notes", props: { level: 1 } },
{ type: "paragraph", content: "Date: " },
{ type: "paragraph", content: "Attendees: " },
{ type: "heading", content: "Agenda", props: { level: 2 } },
{ type: "bulletListItem", content: "Item 1" },
{ type: "bulletListItem", content: "Item 2" },
{ type: "heading", content: "Action Items", props: { level: 2 } },
{ type: "bulletListItem", content: "Action 1" },
],
article: [
{ type: "heading", content: "Article Title", props: { level: 1 } },
{ type: "paragraph", content: "Introduction paragraph..." },
{ type: "heading", content: "Main Content", props: { level: 2 } },
{ type: "paragraph", content: "Content goes here..." },
],
};
const template = templates[templateName];
if (template) {
// Insert at the end of the document
const lastBlock = editor.document[editor.document.length - 1];
editor.insertBlocks(template, lastBlock.id, "after");
}
}
Example 5: Bulk Operations
function bulkUpdateBlocks(
editor: BlockNoteEditor,
filter: (block: Block) => boolean,
update: PartialBlock,
) {
const blocksToUpdate: string[] = [];
editor.forEachBlock((block) => {
if (filter(block)) {
blocksToUpdate.push(block.id);
}
});
// Update all matching blocks
blocksToUpdate.forEach((blockId) => {
editor.updateBlock(blockId, update);
});
console.log(`Updated ${blocksToUpdate.length} blocks`);
}
// Usage: Make all paragraphs bold
bulkUpdateBlocks(editor, (block) => block.type === "paragraph", {
props: { textStyle: "bold" },
});
Example 6: Document Restructuring
function restructureDocument(editor: BlockNoteEditor) {
const blocks = editor.document;
const newStructure: PartialBlock[] = [];
blocks.forEach((block) => {
if (block.type === "heading" && block.props.level === 1) {
// Keep H1 headings as top-level
newStructure.push({
type: block.type,
content: block.content,
props: block.props,
});
} else if (block.type === "paragraph") {
// Nest paragraphs under the last H1 heading
if (
newStructure.length > 0 &&
newStructure[newStructure.length - 1].type === "heading"
) {
const lastHeading = newStructure[newStructure.length - 1];
if (!lastHeading.children) {
lastHeading.children = [];
}
lastHeading.children.push({
type: block.type,
content: block.content,
});
}
}
});
// Replace the entire document
editor.replaceBlocks(
blocks.map((b) => b.id),
newStructure,
);
}
Best Practices
1. Use Block Identifiers Consistently
// ✅ Good - Use block objects when you have them
const block = editor.getBlock("block-123");
const nextBlock = editor.getNextBlock(block);
// ✅ Good - Use IDs when you only have the ID
editor.updateBlock("block-123", { content: "Updated" });
2. Handle Errors Gracefully
try {
editor.updateBlock("non-existent-block", { content: "New content" });
} catch (error) {
console.error("Block not found:", error);
}
3. Batch Related Operations
// ✅ Good - Group related operations
editor.insertBlocks(
[
{ type: "heading", content: "Section", props: { level: 2 } },
{ type: "paragraph", content: "Content" },
],
referenceBlock,
"after",
);
// ❌ Avoid - Multiple separate operations
editor.insertBlocks(
[{ type: "heading", content: "Section", props: { level: 2 } }],
referenceBlock,
"after",
);
editor.insertBlocks(
[{ type: "paragraph", content: "Content" }],
referenceBlock,
"after",
);
4. Validate Block Types
function updateHeadingLevel(
editor: BlockNoteEditor,
blockId: string,
newLevel: number,
) {
const block = editor.getBlock(blockId);
if (block && block.type === "heading") {
editor.updateBlock(blockId, { props: { level: newLevel } });
} else {
console.warn("Block is not a heading");
}
}
These APIs provide powerful tools for programmatically manipulating BlockNote documents, enabling features like templates, bulk operations, document restructuring, and custom workflows.