Obsidian Core Workflow B: UI Components
Overview
Secondary workflow for Obsidian: building modals, views, suggestion popups, and custom UI elements.
Prerequisites
- Completed
obsidian-install-authsetup - Familiarity with
obsidian-core-workflow-a - Understanding of DOM manipulation
Instructions
Step 1: Modal Dialogs
import { App, Modal, Setting } from 'obsidian'; // Simple confirmation modal export class ConfirmModal extends Modal { private result: boolean = false; private onSubmit: (result: boolean) => void; private message: string; constructor(app: App, message: string, onSubmit: (result: boolean) => void) { super(app); this.message = message; this.onSubmit = onSubmit; } onOpen() { const { contentEl } = this; contentEl.createEl('h2', { text: 'Confirm Action' }); contentEl.createEl('p', { text: this.message }); new Setting(contentEl) .addButton(btn => btn .setButtonText('Cancel') .onClick(() => { this.result = false; this.close(); })) .addButton(btn => btn .setButtonText('Confirm') .setCta() .onClick(() => { this.result = true; this.close(); })); } onClose() { this.onSubmit(this.result); } } // Form input modal export class InputModal extends Modal { private value: string = ''; private onSubmit: (value: string | null) => void; private placeholder: string; private title: string; constructor( app: App, title: string, placeholder: string, onSubmit: (value: string | null) => void ) { super(app); this.title = title; this.placeholder = placeholder; this.onSubmit = onSubmit; } onOpen() { const { contentEl } = this; contentEl.createEl('h2', { text: this.title }); new Setting(contentEl) .setName('Input') .addText(text => text .setPlaceholder(this.placeholder) .onChange(value => this.value = value)); new Setting(contentEl) .addButton(btn => btn .setButtonText('Cancel') .onClick(() => { this.close(); this.onSubmit(null); })) .addButton(btn => btn .setButtonText('Submit') .setCta() .onClick(() => { this.close(); this.onSubmit(this.value); })); } onClose() { const { contentEl } = this; contentEl.empty(); } }
Step 2: Suggestion Popups (FuzzySuggestModal)
import { App, FuzzySuggestModal, TFile } from 'obsidian'; // File picker modal export class FileSuggestModal extends FuzzySuggestModal<TFile> { private onSelect: (file: TFile) => void; constructor(app: App, onSelect: (file: TFile) => void) { super(app); this.onSelect = onSelect; } getItems(): TFile[] { return this.app.vault.getMarkdownFiles(); } getItemText(file: TFile): string { return file.path; } onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent): void { this.onSelect(file); } } // Generic suggestion modal export class SuggestModal<T> extends FuzzySuggestModal<T> { private items: T[]; private getText: (item: T) => string; private onSelect: (item: T) => void; constructor( app: App, items: T[], getText: (item: T) => string, onSelect: (item: T) => void ) { super(app); this.items = items; this.getText = getText; this.onSelect = onSelect; } getItems(): T[] { return this.items; } getItemText(item: T): string { return this.getText(item); } onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void { this.onSelect(item); } } // Usage: new SuggestModal( this.app, ['Option 1', 'Option 2', 'Option 3'], (item) => item, (selected) => console.log('Selected:', selected) ).open();
Step 3: Custom Views (Leaf Views)
import { ItemView, WorkspaceLeaf } from 'obsidian'; export const VIEW_TYPE_CUSTOM = 'custom-view'; export class CustomView extends ItemView { constructor(leaf: WorkspaceLeaf) { super(leaf); } getViewType(): string { return VIEW_TYPE_CUSTOM; } getDisplayText(): string { return 'Custom View'; } getIcon(): string { return 'dice'; } async onOpen() { const container = this.containerEl.children[1]; container.empty(); // Add content container.createEl('h4', { text: 'Custom View Title' }); const content = container.createDiv({ cls: 'custom-view-content' }); content.createEl('p', { text: 'This is a custom view!' }); // Add interactive elements const button = content.createEl('button', { text: 'Click me' }); button.addEventListener('click', () => { console.log('Button clicked!'); }); } async onClose() { // Cleanup } } // Register in main.ts: export default class MyPlugin extends Plugin { async onload() { this.registerView( VIEW_TYPE_CUSTOM, (leaf) => new CustomView(leaf) ); // Add ribbon icon to open view this.addRibbonIcon('dice', 'Open Custom View', () => { this.activateView(); }); // Add command this.addCommand({ id: 'open-custom-view', name: 'Open Custom View', callback: () => this.activateView(), }); } async activateView() { const { workspace } = this.app; let leaf = workspace.getLeavesOfType(VIEW_TYPE_CUSTOM)[0]; if (!leaf) { // Create new leaf in right sidebar leaf = workspace.getRightLeaf(false); await leaf.setViewState({ type: VIEW_TYPE_CUSTOM, active: true, }); } workspace.revealLeaf(leaf); } onunload() { // Clean up view this.app.workspace.detachLeavesOfType(VIEW_TYPE_CUSTOM); } }
Step 4: Editor Extensions (CodeMirror 6)
import { EditorView, ViewPlugin, Decoration, DecorationSet } from '@codemirror/view'; import { RangeSetBuilder } from '@codemirror/state'; import { syntaxTree } from '@codemirror/language'; // Custom decoration plugin const highlightPlugin = ViewPlugin.fromClass(class { decorations: DecorationSet; constructor(view: EditorView) { this.decorations = this.buildDecorations(view); } update(update: any) { if (update.docChanged || update.viewportChanged) { this.decorations = this.buildDecorations(update.view); } } buildDecorations(view: EditorView): DecorationSet { const builder = new RangeSetBuilder<Decoration>(); for (const { from, to } of view.visibleRanges) { syntaxTree(view.state).iterate({ from, to, enter: (node) => { if (node.name === 'HyperLink') { builder.add( node.from, node.to, Decoration.mark({ class: 'custom-highlight' }) ); } }, }); } return builder.finish(); } }, { decorations: (v) => v.decorations, }); // Register in plugin: this.registerEditorExtension(highlightPlugin);
Step 5: Context Menus
import { Menu, TFile, TFolder } from 'obsidian'; // Add to file menu this.registerEvent( this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile) => { if (file instanceof TFile && file.extension === 'md') { menu.addItem((item) => { item .setTitle('My Custom Action') .setIcon('star') .onClick(async () => { // Handle click console.log('File:', file.path); }); }); } }) ); // Add to editor context menu this.registerEvent( this.app.workspace.on('editor-menu', (menu: Menu, editor: Editor, view: MarkdownView) => { menu.addItem((item) => { item .setTitle('Insert Timestamp') .setIcon('clock') .onClick(() => { editor.replaceSelection(new Date().toISOString()); }); }); }) );
Output
- Modal dialogs for user input
- Suggestion popups with fuzzy search
- Custom sidebar views
- Editor decorations
- Context menus
Error Handling
| Error | Cause | Solution |
|---|---|---|
| View not showing | Not registered | Call registerView in onload |
| Modal closes immediately | Event propagation | Stop event propagation |
| Decorations not updating | Missing update handler | Implement update method |
| Menu item missing | Wrong event | Verify event type |
Examples
Progress Modal
export class ProgressModal extends Modal { private progressEl: HTMLElement; private textEl: HTMLElement; onOpen() { const { contentEl } = this; contentEl.createEl('h2', { text: 'Processing...' }); this.textEl = contentEl.createEl('p', { text: 'Starting...' }); const progressContainer = contentEl.createDiv({ cls: 'progress-container' }); this.progressEl = progressContainer.createDiv({ cls: 'progress-bar' }); this.progressEl.style.width = '0%'; } setProgress(percent: number, text?: string) { this.progressEl.style.width = `${percent}%`; if (text) this.textEl.setText(text); } onClose() { this.contentEl.empty(); } } // Usage: const modal = new ProgressModal(this.app); modal.open(); for (let i = 0; i <= 100; i += 10) { modal.setProgress(i, `Processing ${i}%`); await sleep(100); } modal.close();
Sidebar Styling
/* styles.css */ .custom-view-content { padding: 16px; } .custom-view-content h4 { margin-bottom: 12px; } .custom-view-content button { margin-top: 8px; }
Resources
Next Steps
For common errors, see obsidian-common-errors.