This page predates the current package split. Use the current package guides for checked installation, public imports, and onboarding examples. The detailed examples below have not all been revalidated.
Custom Operations Guide
Operations are the atomic units of document modification. While extensions use operations through transactions, you can also create your own custom operations for domain-specific needs.
What are Operations?
Operations are the lowest-level actions that modify documents. They're executed within transactions and interact directly with DataStore.
Operation vs Extension:
- Operation: Atomic document modification (Model package level)
- Extension: Commands that use operations (Editor level)
When to Create Custom Operations
Create custom operations when:
- You need domain-specific document modifications
- You want reusable operations across multiple extensions
- You need operations that are more efficient than composing existing ones
- You're building a library of operations for others to use
Defining Operations
Operations are defined using defineOperation from @barocss/model/operations.
Basic Operation Definition
import { defineOperation } from '@barocss/model/operations';
import type { TransactionContext } from '@barocss/model';
import type { INode } from '@barocss/datastore';
// 1. Define Operation Type
export interface SetColorOperation {
type: 'setColor';
payload: {
nodeId: string;
color: string;
};
}
// 2. Define Operation Execution Logic
defineOperation('setColor', async (
operation: SetColorOperation,
context: TransactionContext
) => {
const { nodeId, color } = operation.payload;
// Get node
const node = context.dataStore.getNode(nodeId);
if (!node) {
throw new Error(`Node not found: ${nodeId}`);
}
// Store old value for undo
const oldColor = node.attributes?.color;
// Perform modification
context.dataStore.updateNode(nodeId, {
attributes: {
...node.attributes,
color
}
});
// Return result
return {
ok: true,
data: context.dataStore.getNode(nodeId),
inverse: {
type: 'setColor',
payload: { nodeId, color: oldColor }
}
};
});
Operation Structure
interface OperationResult {
ok: boolean;
data?: any;
inverse?: Operation; // For undo support
error?: string;
}
Operation execution:
- Receives operation and TransactionContext
- Accesses DataStore through context
- Modifies document
- Returns result with optional inverse operation
Registering Operations
Operations must be imported to be registered:
// packages/model/src/operations/register-operations.ts
import './setColor';
import './toggleHighlight';
// Import other operations...
Important: Operations must be imported before use:
// At app initialization
import '@barocss/model/operations/register-operations';
// Now operations are available
await transaction(editor, [
{ type: 'setColor', payload: { nodeId: 'p1', color: 'red' } }
]).commit();
Defining Operation DSL
Operation DSL provides helper functions for convenient use in transactions.
DSL Definition
import { defineOperationDSL } from '@barocss/model/operations';
export const setColor = defineOperationDSL(
(nodeId: string, color: string) => ({
type: 'setColor',
payload: { nodeId, color }
}),
{ atom: false, category: 'content' }
);
Using DSL
import { transaction, control, setColor } from '@barocss/model';
const ops = [
...control('p1', [
setColor('p1', 'red')
])
];
await transaction(editor, ops).commit();
DSL benefits:
- Type-safe function calls
- Better developer experience
- Easier to compose operations
- Clearer intent
Complete Example: Highlight Operation
Let's create a complete highlight operation with DSL:
Step 1: Define Operation
// operations/toggleHighlight.ts
import { defineOperation } from '@barocss/model/operations';
import type { TransactionContext } from '@barocss/model';
export interface ToggleHighlightOperation {
type: 'toggleHighlight';
payload: {
nodeId: string;
range: [number, number];
color?: string;
};
}
defineOperation('toggleHighlight', async (
operation: ToggleHighlightOperation,
context: TransactionContext
) => {
const { nodeId, range, color = 'yellow' } = operation.payload;
const node = context.dataStore.getNode(nodeId);
if (!node || typeof node.text !== 'string') {
throw new Error(`Invalid node for toggleHighlight: ${nodeId}`);
}
const marks = node.marks || [];
const [start, end] = range;
// Check if highlight mark exists
const existingMark = marks.find(
m => m.type === 'highlight' &&
m.range[0] === start &&
m.range[1] === end &&
m.attrs?.color === color
);
if (existingMark) {
// Remove mark
const newMarks = marks.filter(m => m !== existingMark);
context.dataStore.updateNode(nodeId, { marks: newMarks });
return {
ok: true,
data: context.dataStore.getNode(nodeId),
inverse: {
type: 'toggleHighlight',
payload: { nodeId, range, color }
}
};
} else {
// Add mark
const newMark = {
type: 'highlight',
range: [start, end],
attrs: { color }
};
context.dataStore.updateNode(nodeId, {
marks: [...marks, newMark]
});
return {
ok: true,
data: context.dataStore.getNode(nodeId),
inverse: {
type: 'toggleHighlight',
payload: { nodeId, range, color }
}
};
}
});
Step 2: Define DSL
// operations-dsl/toggleHighlight.ts
import { defineOperationDSL } from '@barocss/model/operations';
export const toggleHighlight = defineOperationDSL(
(range: [number, number], color?: string) => ({
type: 'toggleHighlight',
payload: { range, color }
}),
{ atom: false, category: 'format' }
);
Step 3: Register Operation
// operations/register-operations.ts
import './toggleHighlight';
// operations-dsl/index.ts
export * from './toggleHighlight';
Step 4: Use in Extension
import { transaction, control, toggleHighlight } from '@barocss/model';
// In extension command
const result = await transaction(editor, [
...control(selection.startNodeId, [
toggleHighlight([startOffset, endOffset], 'yellow')
])
]).commit();
Operation Best Practices
1. Always Validate Inputs
defineOperation('myOperation', async (operation, context) => {
const { nodeId } = operation.payload;
// Validate node exists
const node = context.dataStore.getNode(nodeId);
if (!node) {
throw new Error(`Node not found: ${nodeId}`);
}
// Validate node type
if (node.stype !== 'expected-type') {
throw new Error(`Invalid node type: ${node.stype}`);
}
// Proceed with operation
// ...
});
2. Provide Inverse Operations
return {
ok: true,
data: updatedNode,
inverse: {
type: 'myOperation',
payload: { /* old values */ }
}
};
Inverse operations enable:
- Undo/redo support
- Transaction rollback
- History management
3. Use TransactionContext
defineOperation('myOperation', async (operation, context) => {
// Access DataStore
const node = context.dataStore.getNode(nodeId);
// Access Schema (if needed)
const schema = context.schema;
// Access Editor (if needed)
const editor = context.editor;
// Perform operation
// ...
});
4. Handle Errors Gracefully
defineOperation('myOperation', async (operation, context) => {
try {
// Operation logic
return { ok: true, data: result };
} catch (error) {
return {
ok: false,
error: error.message
};
}
});
5. Keep Operations Atomic
// ✅ Good: Single responsibility
defineOperation('setColor', async (operation, context) => {
// Only sets color
});
// ❌ Bad: Multiple responsibilities
defineOperation('setColorAndSize', async (operation, context) => {
// Sets both color and size - should be two operations
});
Operation Categories
Operations can be categorized for better organization:
defineOperationDSL(
(/* params */) => ({ type: 'myOperation', payload: {} }),
{
atom: false, // Can be split into smaller operations
category: 'content' | 'format' | 'structure' | 'custom'
}
);
Categories:
- content: Text and content modifications
- format: Formatting (marks, styles)
- structure: Node structure changes
- custom: Domain-specific operations
Testing Operations
Test operations in isolation:
import { describe, it, expect } from 'vitest';
import { DataStore } from '@barocss/datastore';
import { TransactionManager } from '@barocss/model';
import './operations/register-operations';
describe('setColor operation', () => {
it('should set node color', async () => {
const dataStore = new DataStore(undefined, schema);
const tm = new TransactionManager(dataStore);
// Create node
dataStore.createNode({
sid: 'p1',
stype: 'paragraph',
content: []
});
// Execute operation
const transaction = tm.createBuilder('test')
.addOperation({
type: 'setColor',
payload: { nodeId: 'p1', color: 'red' }
});
const result = await transaction.commit();
expect(result.success).toBe(true);
// Verify
const node = dataStore.getNode('p1');
expect(node.attributes?.color).toBe('red');
});
});
Related
- Extension Design Guide - How extensions use operations
- Architecture: Model - Model package details
- Core Concepts: Schema & Model - Understanding the model