Obsidian’s event system allows plugins to react to changes in the vault, workspace, and other components. Understanding how to properly register and handle events is crucial for building responsive plugins.
Event Registration
The Component class (which Plugin extends) provides methods for registering events that are automatically cleaned up when the plugin unloads.
registerEvent()
Use registerEvent() to register event listeners on Obsidian objects:
this . registerEvent (
this . app . workspace . on ( 'file-open' , ( file ) => {
console . log ( 'File opened:' , file ?. path );
})
);
registerEvent() returns the EventRef, but you typically don’t need to store it. The Component system handles cleanup automatically.
registerDomEvent()
Use registerDomEvent() to register DOM event listeners:
// Document events
this . registerDomEvent ( document , 'click' , ( evt : MouseEvent ) => {
console . log ( 'Document clicked' );
});
// Window events
this . registerDomEvent ( window , 'resize' , ( evt : UIEvent ) => {
console . log ( 'Window resized' );
});
// Element events
const button = containerEl . createEl ( 'button' );
this . registerDomEvent ( button , 'click' , ( evt : MouseEvent ) => {
console . log ( 'Button clicked' );
});
Only use registerDomEvent() for elements that persist after your plugin unloads (like window or document). For elements created by your plugin, regular event listeners are automatically cleaned up with the element.
registerInterval()
Use registerInterval() to register intervals that are automatically cleared:
this . registerInterval (
window . setInterval (() => {
console . log ( 'Periodic check' );
}, 5000 )
);
Always use window.setInterval() instead of setInterval() to avoid TypeScript confusion between Node.js and browser APIs.
Common Events
Workspace Events
The Workspace emits events related to UI changes:
// File opened
this . registerEvent (
this . app . workspace . on ( 'file-open' , ( file : TFile | null ) => {
if ( file ) {
console . log ( 'Opened:' , file . path );
}
})
);
// Active leaf changed
this . registerEvent (
this . app . workspace . on ( 'active-leaf-change' , ( leaf : WorkspaceLeaf ) => {
console . log ( 'Active leaf changed' );
})
);
// Layout changed
this . registerEvent (
this . app . workspace . on ( 'layout-change' , () => {
console . log ( 'Layout changed' );
})
);
// Window opened (popout windows)
this . registerEvent (
this . app . workspace . on ( 'window-open' , ( win : WorkspaceWindow ) => {
console . log ( 'New window opened' );
})
);
// Window closed (popout windows)
this . registerEvent (
this . app . workspace . on ( 'window-close' , ( win : WorkspaceWindow ) => {
console . log ( 'Window closed' );
})
);
// Quick preview shown
this . registerEvent (
this . app . workspace . on ( 'quick-preview' , ( file : TFile , data : string ) => {
console . log ( 'Quick preview:' , file . path );
})
);
Vault Events
The Vault emits events related to file system changes:
// File created
this . registerEvent (
this . app . vault . on ( 'create' , ( file : TAbstractFile ) => {
console . log ( 'Created:' , file . path );
})
);
// File modified
this . registerEvent (
this . app . vault . on ( 'modify' , ( file : TAbstractFile ) => {
console . log ( 'Modified:' , file . path );
})
);
// File deleted
this . registerEvent (
this . app . vault . on ( 'delete' , ( file : TAbstractFile ) => {
console . log ( 'Deleted:' , file . path );
})
);
// File renamed
this . registerEvent (
this . app . vault . on ( 'rename' , ( file : TAbstractFile , oldPath : string ) => {
console . log ( 'Renamed:' , oldPath , '->' , file . path );
})
);
Vault events are fired for all file types, not just markdown files. Use file instanceof TFile or check file.extension to filter.
The MetadataCache emits events when cached metadata changes:
// Metadata changed
this . registerEvent (
this . app . metadataCache . on ( 'changed' , ( file : TFile ) => {
const cache = this . app . metadataCache . getFileCache ( file );
console . log ( 'Metadata updated for:' , file . path );
})
);
// Cache resolved (on startup)
this . registerEvent (
this . app . metadataCache . on ( 'resolved' , () => {
console . log ( 'Metadata cache fully loaded' );
})
);
The changed event is emitted asynchronously after a file is modified. Don’t rely on immediate metadata updates.
Event Patterns
Debouncing Events
For events that fire frequently, use debouncing to limit processing:
import { debounce } from 'obsidian' ;
export default class MyPlugin extends Plugin {
async onload () {
// Debounce the handler
const debouncedHandler = debounce (
( file : TFile ) => {
console . log ( 'Processing:' , file . path );
},
1000 , // Wait 1 second
true // Reset timer on each call
);
this . registerEvent (
this . app . vault . on ( 'modify' , ( file ) => {
if ( file instanceof TFile ) {
debouncedHandler ( file );
}
})
);
}
}
Filtering Events
Filter events to only process relevant files:
this . registerEvent (
this . app . vault . on ( 'modify' , ( file ) => {
// Only process markdown files
if ( ! ( file instanceof TFile ) || file . extension !== 'md' ) {
return ;
}
// Only process files in specific folder
if ( ! file . path . startsWith ( 'notes/' )) {
return ;
}
// Process the file
this . processFile ( file );
})
);
Combining Multiple Events
React to multiple related events:
const handleFileChange = async ( file : TFile ) => {
console . log ( 'File changed:' , file . path );
await this . updateIndex ( file );
};
this . registerEvent (
this . app . vault . on ( 'create' , ( file ) => {
if ( file instanceof TFile ) {
handleFileChange ( file );
}
})
);
this . registerEvent (
this . app . vault . on ( 'modify' , ( file ) => {
if ( file instanceof TFile ) {
handleFileChange ( file );
}
})
);
this . registerEvent (
this . app . vault . on ( 'delete' , ( file ) => {
this . removeFromIndex ( file . path );
})
);
Editor-Specific Events
For editor-related functionality, use the CodeMirror state field or editor callbacks:
import { editorInfoField } from 'obsidian' ;
import { EditorView , ViewUpdate } from '@codemirror/view' ;
const updateListener = EditorView . updateListener . of (( update : ViewUpdate ) => {
if ( update . docChanged ) {
console . log ( 'Document changed' );
}
if ( update . selectionSet ) {
console . log ( 'Selection changed' );
}
});
this . registerEditorExtension ( updateListener );
Custom Events
You can create your own event system using the Events class:
import { Events } from 'obsidian' ;
class MyCustomEvents extends Events {
// Type-safe event names
on ( name : 'custom-event' , callback : ( data : string ) => any ) : EventRef ;
on ( name : string , callback : ( ... data : any ) => any ) : EventRef {
return super . on ( name , callback );
}
}
export default class MyPlugin extends Plugin {
events : MyCustomEvents ;
async onload () {
this . events = new MyCustomEvents ();
// Other plugins can listen
this . registerEvent (
this . events . on ( 'custom-event' , ( data ) => {
console . log ( 'Custom event:' , data );
})
);
// Trigger the event
this . events . trigger ( 'custom-event' , 'Hello!' );
}
}
Best Practices
Always Register Always use registerEvent(), registerDomEvent(), and registerInterval() for automatic cleanup.
Filter Early Filter events as early as possible to avoid unnecessary processing.
Debounce Frequent Events Use debounce() for events that fire frequently, like modify events.
Type Guards Use instanceof checks to distinguish between TFile and TFolder.
Complete Example
import { Plugin , TFile , TFolder , debounce } from 'obsidian' ;
export default class EventExample extends Plugin {
async onload () {
// Debounced modify handler
const debouncedModify = debounce (
async ( file : TFile ) => {
const cache = this . app . metadataCache . getFileCache ( file );
console . log ( 'Headings:' , cache ?. headings ?. length );
},
1000 ,
true
);
// Vault events
this . registerEvent (
this . app . vault . on ( 'create' , ( file ) => {
if ( file instanceof TFile && file . extension === 'md' ) {
console . log ( 'New note created:' , file . path );
}
})
);
this . registerEvent (
this . app . vault . on ( 'modify' , ( file ) => {
if ( file instanceof TFile && file . extension === 'md' ) {
debouncedModify ( file );
}
})
);
this . registerEvent (
this . app . vault . on ( 'rename' , ( file , oldPath ) => {
console . log ( 'Renamed:' , oldPath , '->' , file . path );
})
);
// Workspace events
this . registerEvent (
this . app . workspace . on ( 'file-open' , ( file ) => {
if ( file ) {
console . log ( 'Opened:' , file . path );
}
})
);
this . registerEvent (
this . app . workspace . on ( 'layout-change' , () => {
console . log ( 'Layout changed' );
})
);
// Metadata cache events
this . registerEvent (
this . app . metadataCache . on ( 'changed' , ( file ) => {
console . log ( 'Metadata updated:' , file . path );
})
);
// DOM events
this . registerDomEvent ( document , 'click' , ( evt : MouseEvent ) => {
console . log ( 'Click at:' , evt . clientX , evt . clientY );
});
// Interval
this . registerInterval (
window . setInterval (() => {
console . log ( 'Periodic check at:' , new Date (). toISOString ());
}, 60000 ) // Every minute
);
}
}
Plugin Lifecycle Learn about lifecycle methods and component registration
App Architecture Understand the modules that emit events