Plugin Helpers
Obsidian Dev Utils provides some helpers to simplify your own Obsidian plugin.
- Sample Plugin Extended - sample with different technologies included.
- Obsidian Plugin Yeoman Generator - generator to make plugin from scratch.
The links below contains the full documentation. Here in the docs we mention only the most important ones.
PluginBase is a base class for plugins, that has some additional useful features to standard Obsidian Plugin class.
export class FooPlugin extends PluginBase {}The most important methods in the execution order:
onload()- usually you don’t need to override it.onloadImpl()onLoadSettings()onLayoutReady()onSaveSettings()onExternalSettingsChange()- usually you don’t need to override it.onunload()- usually you don’t need to override it.
plugin.pluginNoticeComponent shows notices prefixed with the plugin name and tied to the plugin lifecycle. Prefer it over constructing an Obsidian Notice directly.
plugin.pluginNoticeComponent.showNotice('Something happened');showNotice(message, options?) accepts these options:
isPermanent(defaultfalse) — the notice stays until it is replaced, the plugin reloads, or the user dismisses it. There is at most one permanent notice per plugin.isReusable(defaulttrue) — the notice occupies the single per-plugin reusable slot, so the next reusable notice hides it. Passfalsefor a standalone notice that no later notice hides (multiple standalone notices coexist); standalone notices are still hidden together on unload.requiresCloseConfirmation(defaultfalse) — a hard-to-close notice: it never dismisses on a stray click and instead shows a close (X) button whose click opens a confirmation modal, dismissing the notice only if confirmed. It is shown with an infinite duration and is standalone (impliesisReusable: false).onHide— a callback invoked the first time the notice is hidden, whether by the user closing it, by a later reusable notice replacing it, by its duration elapsing, or on unload.
The implication rules are enforced — a contradictory combination throws:
- A permanent notice must be reusable, so
isPermanent: truetogether withisReusable: falsethrows. - A hard-to-close notice is standalone, so
requiresCloseConfirmation: truetogether withisReusable: truethrows.
export class FooPluginSettingsComponent extends PluginSettingsComponentBase<FooPluginSettings> {}The most important methods in the execution order:
createDefaultSettings()registerValidators()onLoadRecord()onSavingRecord()
export class FooPluginSettingsTab extends PluginSettingsTabBase<FooPluginSettings> {}Obsidian 1.13 renders a settings tab from the definitions it returns, so a tab describes its rows instead of
building them into a container. Override getSettingDefinitionItems() and build each row with settingEx(),
grouping them with settingGroupEx():
export class FooPluginSettingsTab extends PluginSettingsTabBase<FooPluginSettings> { protected override getSettingDefinitionItems(): SettingDefinitionItem[] { return [ this.settingGroupEx({ heading: 'Invalid characters', items: [ this.settingEx({ desc: 'How to process invalid characters in the new title.', name: 'Invalid characters action', render: (setting) => { setting.addDropdown((dropdown) => { dropdown.addOptions({ Remove: 'Remove', Replace: 'Replace' }); this.bind({ onChanged: () => { this.refreshDomState(); }, propertyName: 'invalidCharacterAction', valueComponent: dropdown }); }); } }), this.settingEx({ disabled: () => this.pluginSettingsComponent.settings.invalidCharacterAction !== 'Replace', name: 'Replacement character', render: (setting) => { setting.addText((text) => { this.bind({ propertyName: 'replacementCharacter', valueComponent: text }); }); } }) ] }) ]; }}Notes:
- The
settingarendercallback receives is a fullSettingEx, so every custom component adder (addNumber(),addMultipleText(),addCodeHighlighter(), …) andbind()work exactly as they do in an imperative tab. getSettingDefinitionItems()must be a pure builder. Obsidian calls it when the tab is registered — at plugin load, long before the tab is opened — to index the settings for search. Put anything with a side effect inside arendercallback, which runs only when the row is actually rendered.disabledandvisibleaccept a function, re-evaluated on every render and on everyrefreshDomState(). PreferrefreshDomState()when only those predicates change: it toggles the rendered DOM in place. Userefresh()only when the structure changes, i.e. rows are added or removed.- Native
controldefinitions are supported too —getControlValue()/setControlValue()are wired to the settings component — butrender+bind()is the richer path: it carries validation messages, the reset-when-empty behavior, placeholders for default values, and the value converters. - A tab that has not migrated keeps working: leave
getSettingDefinitionItems()alone and overridedisplayLegacy(), which Obsidian falls back to when the definitions are empty.
The most important methods in the execution order:
getSettingDefinitionItems()(ordisplayLegacy()for the imperative fallback)settingGroupEx()/settingEx()bind()
app.vault.adapter.rmdir(path, recursive) does not honor recursive: false, and the two adapters get it wrong in opposite directions:
CapacitorAdapter(mobile) does not accept the argument at all - it always removes recursively, so a non-recursive call silently deletes the folder and everything under it.FileSystemAdapter(desktop) forwards it tofs.promises.rm(path, { recursive }), which throwsERR_FS_EISDIRfor any directory whenrecursiveisfalse- so the non-recursive call never succeeds, not even on an empty folder.
RmdirGuardComponent gives both platforms the same, correct behavior while it is loaded: a folder that still has children is refused with an error carrying code: 'ENOTEMPTY' and nothing is deleted, and an empty folder is removed. Calls that already pass recursive: true, and calls whose target is not a folder, are passed through untouched.
Emptiness is decided from the adapter, not from the vault file tree - the tree omits dot-prefixed and otherwise hidden entries, so a folder holding only hidden files reads as empty there and the guard would wave through exactly the deletion it exists to prevent.
It is deliberately opt-in. app.vault.adapter is shared by the whole app, so a library that installed this patch on its own would change rmdir semantics for every other plugin, including the ones that never asked for it. A plugin that deletes folders opts in for itself:
this.addChild(new RmdirGuardComponent(this.app));Check error.code === 'ENOTEMPTY' rather than instanceof - every plugin bundles its own copy of the library, so the error may cross a copy boundary.
SyntaxHighlightingComponent registers your own syntax highlighting languages and unregisters them when it is unloaded. Add it as a child of your plugin: const syntaxHighlightingComponent = this.addChild(new SyntaxHighlightingComponent());.
Obsidian highlights code through two independent registries, so there are two methods:
registerPrismLanguageAsync({ grammar, language })- registers a Prism language, which highlights code in reading view and in every<pre><code>the library renders, including theSettingEx.addCodeHighlighter()field.registerCodeBlockLanguageAsync({ editorMode, language, prismGrammar })- additionally registers a CodeMirror 5 mode, which is what highlights a fenced code block inside the editor.editorModeis the mode name or MIME type the fence is highlighted as, e.g.text/typescript. OmitprismGrammarwhen the fence is replaced in reading view, e.g. rendered as a button.
grammar / prismGrammar accepts three forms:
- a
Grammarobject - the grammar itself. - a
string- the name of an already registered language to alias, e.g.'typescript'. - a factory
({ prism, requirePrismLanguage }) => Grammar- builds the grammar from the loaded Prism module, so you never need to callloadPrism()yourself. UserequirePrismLanguage('javascript')to nest an existing language’s grammar into your own.
A missing language always throws - both when aliasing a language that is not registered and from requirePrismLanguage() - so a typo fails loudly instead of silently leaving your code without highlighting.
await syntaxHighlightingComponent.registerCodeBlockLanguageAsync({ editorMode: 'text/typescript', language: 'my-language', prismGrammar: 'typescript'});Working with plugin settings
Section titled “Working with plugin settings”Most of the times, it’s enough to use plugin.settings which is just an alias to plugin.settingsComponent.settingsState.effectiveValues.
For more advanced scenarios, you can use plugin.settingsComponent.settingsState with the following properties:
inputValues- values as been set, even if they don’t pass validation.effectiveValues- set values, if they pass validation, or default value, otherwise.validationMessages- contains validation messages for each setting properties that don’t pass validation.