| name | obsidian-core-workflow-b |
| description | Execute Obsidian secondary workflow: UI components and user interaction.
Use when building modals, views, suggestions, or custom UI elements.
Trigger with phrases like "obsidian modal", "obsidian UI",
"obsidian view", "obsidian custom interface".
|
| allowed-tools | Read, Write, Edit, Bash(npm:*), Grep |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Obsidian Core Workflow B: UI Components
Overview
Secondary workflow for Obsidian: building modals, views, suggestion popups, and custom UI elements.
Prerequisites
- Completed
obsidian-install-auth setup
- Familiarity with
obsidian-core-workflow-a
- Understanding of DOM manipulation
Instructions
Step 1: Modal Dialogs
import { App, Modal, Setting } from 'obsidian';
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);
}
}
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';
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);
}
}
<T> <T> {
: T[];
: ;
: ;
() {
(app);
. = items;
. = getText;
. = onSelect;
}
(): T[] {
.;
}
(: T): {
.(item);
}
(: T, : | ): {
.(item);
}
}
(
.,
[, , ],
item,
.(, selected)
).();
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();
container.createEl('h4', { text: 'Custom View Title' });
const content = container.createDiv({ cls: 'custom-view-content' });
content.createEl(, { : });
button = content.(, { : });
button.(, {
.();
});
}
() {
}
}
{
() {
.(
,
(leaf)
);
.(, , {
.();
});
.({
: ,
: ,
: .(),
});
}
() {
{ workspace } = .;
leaf = workspace.()[];
(!leaf) {
leaf = workspace.();
leaf.({
: ,
: ,
});
}
workspace.(leaf);
}
() {
...();
}
}
Step 4: Editor Extensions (CodeMirror 6)
import { EditorView, ViewPlugin, Decoration, DecorationSet } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
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 ( { , to } view.) {
(view.).({
,
to,
: {
(node. === ) {
builder.(
node.,
node.,
.({ : })
);
}
},
});
}
builder.();
}
}, {
: v.,
});
.(highlightPlugin);
Step 5: Context Menus
import { Menu, TFile, TFolder } from 'obsidian';
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 () => {
console.log('File:', file.path);
});
});
}
})
);
this.registerEvent(
this.app.workspace.on('editor-menu', (menu: Menu, : , : ) => {
menu.( {
item
.()
.()
.( {
editor.( ().());
});
});
})
);
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) ..(text);
}
() {
..();
}
}
modal = (.);
modal.();
( i = ; i <= ; i += ) {
modal.(i, );
();
}
modal.();
Sidebar Styling
.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.