Yoopta Editor provides a comprehensive event system that allows you to listen to and respond to various editor events. This is essential for implementing features like auto-save, analytics, real-time collaboration, and custom UI updates.
Event Types
Yoopta Editor supports several event types:
packages/core/editor/src/editor/types.ts
type YooptaEditorEventKeys =
| 'change'
| 'focus'
| 'blur'
| 'block:copy'
| 'path-change'
| 'decorations:change' ;
type YooptaEventsMap = {
change : YooptaEventChangePayload ;
focus : boolean ;
blur : boolean ;
'block:copy' : YooptaBlockData ;
'path-change' : YooptaPath ;
'decorations:change' : undefined ;
};
Listening to Events
Use the editor.on() method to listen to events:
editor . on ( 'change' , ( payload ) => {
console . log ( 'Editor changed:' , payload );
});
editor . on ( 'focus' , ( isFocused ) => {
console . log ( 'Editor focused:' , isFocused );
});
editor . on ( 'blur' , ( isBlurred ) => {
console . log ( 'Editor blurred:' , isBlurred );
});
Event API Methods
packages/core/editor/src/editor/types.ts
interface YooEditor {
// Register event listener
on : < K extends keyof YooptaEventsMap >(
event : K ,
fn : ( payload : YooptaEventsMap [ K ]) => void ,
) => void ;
// Register one-time listener (fires once then removes)
once : < K extends keyof YooptaEventsMap >(
event : K ,
fn : ( payload : YooptaEventsMap [ K ]) => void ,
) => void ;
// Remove event listener
off : < K extends keyof YooptaEventsMap >(
event : K ,
fn : ( payload : YooptaEventsMap [ K ]) => void ,
) => void ;
// Emit event manually
emit : < K extends keyof YooptaEventsMap >(
event : K ,
payload : YooptaEventsMap [ K ]
) => void ;
}
The event system is built on EventEmitter3, providing a familiar and performant event handling pattern.
Change Event
The most important event - fires whenever editor content changes:
packages/core/editor/src/editor/types.ts
type YooptaEventChangePayload = {
operations : YooptaOperation []; // List of operations performed
value : YooptaContentValue ; // New editor content
};
Usage Example
editor . on ( 'change' , ({ operations , value }) => {
console . log ( 'Operations performed:' , operations . length );
console . log ( 'New content:' , value );
// Auto-save
saveToBackend ( value );
// Update local state
setValue ( value );
// Track analytics
trackEditorChange ({
operationTypes: operations . map ( op => op . type ),
blockCount: Object . keys ( value ). length ,
});
});
Change Operations
The operations array contains all operations that caused the change:
editor . on ( 'change' , ({ operations }) => {
operations . forEach ( op => {
switch ( op . type ) {
case 'insert_block' :
console . log ( 'Block inserted:' , op . block );
break ;
case 'delete_block' :
console . log ( 'Block deleted:' , op . block );
break ;
case 'set_block_value' :
console . log ( 'Block value changed:' , op . id );
break ;
case 'split_block' :
console . log ( 'Block split:' , op . properties . nextBlock );
break ;
case 'merge_block' :
console . log ( 'Block merged:' , op . properties . mergedBlock );
break ;
case 'toggle_block' :
console . log ( 'Block toggled:' , op . properties . toggledBlock );
break ;
case 'move_block' :
console . log ( 'Block moved:' , op . properties );
break ;
// ... more operation types
}
});
});
Use the operations array to implement granular change tracking, undo/redo UI, or collaborative editing features.
Focus Event
Fires when the editor gains focus:
editor . on ( 'focus' , ( isFocused ) => {
console . log ( 'Editor focused:' , isFocused );
// Update UI
setEditorFocused ( true );
// Show floating toolbar
showToolbar ();
});
Blur Event
Fires when the editor loses focus:
editor . on ( 'blur' , ( isBlurred ) => {
console . log ( 'Editor blurred:' , isBlurred );
// Update UI
setEditorFocused ( false );
// Hide floating toolbar
hideToolbar ();
// Save changes
saveContent ();
});
Path Change Event
Fires when the current block path or selection changes:
packages/core/editor/src/editor/types.ts
type YooptaPath = {
current : YooptaPathIndex ; // Current block index
selected ?: number [] | null ; // Selected block indices
selection ?: Selection | null ; // Slate selection
source ?: null | 'selection-box' | 'native-selection' | 'mousemove' | 'keyboard' | 'copy-paste' ;
};
Usage Example
editor . on ( 'path-change' , ( path ) => {
console . log ( 'Current block:' , path . current );
console . log ( 'Selected blocks:' , path . selected );
console . log ( 'Selection source:' , path . source );
// Update UI based on current block
if ( path . current !== null ) {
const block = editor . getBlock ({
id: Object . keys ( editor . children )[ path . current ]
});
if ( block ) {
updateBlockTypeUI ( block . type );
updateBlockDepthUI ( block . meta . depth );
}
}
// Handle multi-block selection
if ( path . selected && path . selected . length > 1 ) {
showMultiBlockActions ();
} else {
hideMultiBlockActions ();
}
});
Block Copy Event
Fires when a block is copied:
editor . on ( 'block:copy' , ( block ) => {
console . log ( 'Block copied:' , block );
// Track analytics
trackBlockCopy ( block . type );
// Custom clipboard handling
customClipboard . set ( block );
});
Decorations Change Event
Fires when decorations (highlights, cursors, etc.) change:
editor . on ( 'decorations:change' , () => {
console . log ( 'Decorations changed' );
// Useful for collaboration features
// or search highlighting updates
});
Decorations are used for features like collaboration cursors, search highlights, and other non-persistent visual overlays.
One-Time Listeners
Use once() to listen to an event only once:
// Listen only for the first change
editor . once ( 'change' , ({ value }) => {
console . log ( 'First change detected:' , value );
initializeTracking ();
});
// Listen for the first focus
editor . once ( 'focus' , () => {
console . log ( 'Editor focused for the first time' );
showOnboardingTip ();
});
Removing Event Listeners
Remove event listeners when they’re no longer needed:
const handleChange = ({ value }) => {
console . log ( 'Content changed:' , value );
};
// Add listener
editor . on ( 'change' , handleChange );
// Remove listener
editor . off ( 'change' , handleChange );
Always remove event listeners in cleanup functions (e.g., React’s useEffect cleanup) to prevent memory leaks.
React Example
import { useYooptaEditor } from '@yoopta/editor' ;
import { useEffect } from 'react' ;
function MyComponent () {
const editor = useYooptaEditor ();
useEffect (() => {
const handleChange = ({ value }) => {
console . log ( 'Changed:' , value );
};
// Add listener
editor . on ( 'change' , handleChange );
// Cleanup: remove listener
return () => {
editor . off ( 'change' , handleChange );
};
}, [ editor ]);
return < div > My Component </ div > ;
}
Emitting Custom Events
You can emit events manually:
// Emit change event
editor . emit ( 'change' , {
operations: [],
value: editor . children ,
});
// Emit custom path change
editor . emit ( 'path-change' , {
current: 5 ,
selected: [ 4 , 5 , 6 ],
source: 'keyboard' ,
});
Manual event emission is useful for triggering event handlers after programmatic changes or in testing scenarios.
Real-World Use Cases
Auto-Save
Implement auto-save with debouncing:
import { debounce } from 'lodash' ;
const autoSave = debounce ( async ( content : YooptaContentValue ) => {
try {
await saveToBackend ( content );
console . log ( 'Content auto-saved' );
} catch ( error ) {
console . error ( 'Auto-save failed:' , error );
}
}, 2000 );
editor . on ( 'change' , ({ value }) => {
autoSave ( value );
});
Analytics Tracking
Track user interactions:
editor . on ( 'change' , ({ operations }) => {
operations . forEach ( op => {
switch ( op . type ) {
case 'insert_block' :
analytics . track ( 'block_inserted' , {
blockType: op . block . type ,
});
break ;
case 'delete_block' :
analytics . track ( 'block_deleted' , {
blockType: op . block . type ,
});
break ;
// ... more tracking
}
});
});
editor . on ( 'focus' , () => {
analytics . track ( 'editor_focused' );
});
editor . on ( 'blur' , () => {
analytics . track ( 'editor_blurred' );
});
Word Count
Display real-time word count:
import { useState , useEffect } from 'react' ;
function useWordCount ( editor : YooEditor ) {
const [ wordCount , setWordCount ] = useState ( 0 );
useEffect (() => {
const updateWordCount = ({ value }) => {
const text = editor . getPlainText ( value );
const words = text . trim (). split ( / \s + / ). filter ( Boolean ). length ;
setWordCount ( words );
};
// Initial count
updateWordCount ({ value: editor . children });
// Update on changes
editor . on ( 'change' , updateWordCount );
return () => {
editor . off ( 'change' , updateWordCount );
};
}, [ editor ]);
return wordCount ;
}
// Usage
function WordCounter () {
const editor = useYooptaEditor ();
const wordCount = useWordCount ( editor );
return < div > Word Count : { wordCount } </ div > ;
}
Undo/Redo UI
Show undo/redo button states:
import { useState , useEffect } from 'react' ;
function useHistoryState ( editor : YooEditor ) {
const [ canUndo , setCanUndo ] = useState ( false );
const [ canRedo , setCanRedo ] = useState ( false );
useEffect (() => {
const updateHistoryState = () => {
setCanUndo ( editor . historyStack . undos . length > 0 );
setCanRedo ( editor . historyStack . redos . length > 0 );
};
// Update on every change
editor . on ( 'change' , updateHistoryState );
return () => {
editor . off ( 'change' , updateHistoryState );
};
}, [ editor ]);
return { canUndo , canRedo };
}
// Usage
function HistoryButtons () {
const editor = useYooptaEditor ();
const { canUndo , canRedo } = useHistoryState ( editor );
return (
< div >
< button
onClick = {() => editor.undo()}
disabled = {! canUndo }
>
Undo
</ button >
< button
onClick = {() => editor.redo()}
disabled = {! canRedo }
>
Redo
</ button >
</ div >
);
}
Multi-Block Selection
Handle multi-block selection:
editor . on ( 'path-change' , ( path ) => {
if ( path . selected && path . selected . length > 1 ) {
console . log ( 'Multiple blocks selected:' , path . selected );
// Show batch actions toolbar
showBatchActionsToolbar ({
onDelete : () => {
path . selected . forEach ( index => {
const blockId = Object . keys ( editor . children )[ index ];
editor . deleteBlock ( blockId );
});
},
onDuplicate : () => {
path . selected . forEach ( index => {
const blockId = Object . keys ( editor . children )[ index ];
editor . duplicateBlock ( blockId );
});
},
});
} else {
hideBatchActionsToolbar ();
}
});
Change Detection
Detect specific types of changes:
editor . on ( 'change' , ({ operations }) => {
const hasTextChanges = operations . some (
op => op . type === 'set_block_value' || op . type === 'set_slate'
);
const hasStructureChanges = operations . some (
op => op . type === 'insert_block' ||
op . type === 'delete_block' ||
op . type === 'move_block'
);
if ( hasTextChanges ) {
console . log ( 'Text content changed' );
}
if ( hasStructureChanges ) {
console . log ( 'Document structure changed' );
}
});
Debouncing Events
For performance-critical operations, debounce event handlers:
import { debounce } from 'lodash' ;
const handleChangeDebounced = debounce (({ value }) => {
// Expensive operation
performExpensiveAnalysis ( value );
}, 500 );
editor . on ( 'change' , handleChangeDebounced );
Throttling Events
Throttle rapid events:
import { throttle } from 'lodash' ;
const handlePathChangeThrottled = throttle (( path ) => {
// Limit updates to once per 100ms
updateUI ( path );
}, 100 );
editor . on ( 'path-change' , handlePathChangeThrottled );
Batch Processing
Process multiple operations together:
editor . on ( 'change' , ({ operations }) => {
// Batch process operations
const blockInserts = operations . filter ( op => op . type === 'insert_block' );
const blockDeletes = operations . filter ( op => op . type === 'delete_block' );
if ( blockInserts . length > 0 ) {
handleBatchInserts ( blockInserts );
}
if ( blockDeletes . length > 0 ) {
handleBatchDeletes ( blockDeletes );
}
});
Best Practices
1. Clean Up Listeners
Always remove event listeners when components unmount:
useEffect (() => {
const handler = ( payload ) => { /* ... */ };
editor . on ( 'change' , handler );
return () => editor . off ( 'change' , handler );
}, [ editor ]);
2. Use Named Functions
Use named functions instead of inline functions for easier debugging and cleanup:
// Good
const handleChange = ({ value }) => {
console . log ( 'Changed:' , value );
};
editor . on ( 'change' , handleChange );
editor . off ( 'change' , handleChange ); // Can properly remove
// Bad
editor . on ( 'change' , ({ value }) => {
console . log ( 'Changed:' , value );
});
// Can't remove this listener easily
3. Avoid Heavy Operations
Keep event handlers lightweight:
// Good: Debounced heavy operation
const heavyOperation = debounce ( async ( value ) => {
await processData ( value );
}, 1000 );
editor . on ( 'change' , ({ value }) => {
heavyOperation ( value );
});
// Bad: Heavy operation on every change
editor . on ( 'change' , async ({ value }) => {
await processData ( value ); // Blocks UI
});
4. Check Editor State
Verify editor state before operations:
editor . on ( 'path-change' , ( path ) => {
if ( path . current === null ) return ;
const blocks = Object . values ( editor . children );
if ( path . current >= blocks . length ) return ;
// Safe to proceed
const currentBlock = blocks [ path . current ];
});
5. Handle Errors
Wrap event handlers in try-catch:
editor . on ( 'change' , ({ value }) => {
try {
processChange ( value );
} catch ( error ) {
console . error ( 'Error processing change:' , error );
// Handle error gracefully
}
});
Next Steps
Editor Instance Back to editor API reference
Blocks Learn about block operations
Plugins Build custom plugins with events
Collaboration Implement real-time collaboration