I was fixing a bug in Buttons and ran into an issue that required a custom event. This is how I fixed the problem. This might not be the best solution, but it worked for me!
Inline Buttons are rendered using a registerMarkdownPostProcessor
. The post processor looks for codeblocks in the note that start with the word button
. It then uses the button's ID to look up the parent button in a store. The store is indexed when Obsidian is first loaded.
The problem is that the post processor was trying to render the inline button before the store had finished indexing.
My first solution was to wrap the rendering of the inline button in a setTimeout
. This delayed the store lookup until after the indexing had completed. I shipped this initially, but realized the time it takes to index can really depend on how many buttons someone has. It also feels like a janky hack and not an elegant solution. I took to the OMG #plugin-development Discord channel and @javalent pointed my towards using an event.
This Obsidian API exposes and Events class that can be extended to add custom events. You'll notice that many other parts of the API are already extending Events for their own event handlers.
Events is a pretty simple class that has everything needed to trigger a custom event:
export class Events {
on(name: string, callback: (...data: any) => any, ctx?: any): EventRef;
off(name: string, callback: (...data: any) => any): void;
offref(ref: EventRef): void;
trigger(name: string, ...data: any[]): void;
tryTrigger(evt: EventRef, args: any[]): void;
}
index-complete
Event If I needed to add anything custom to the Events
class I can extend it:
export class StoreEvents extends Events {
constructor() {
super();
}
}
Because I just need the methods in Events
itself and don't want to add anything custom, I'm going to instantiate Events
directly:
private storeEvents = new Events();
private storeEventsRef: EventRef
StoreEventsRef
is going to be our reference to the event listener so that we can turn it off if the plugin is unloaded.
I passed storeEvents
to my initializeButtonStore
function:
initializeButtonStore(this.app, this.storeEvents);
and triggered the index-complete
event when initializeButtonStore
had finished indexing:
localStorage.setItem("buttons", JSON.stringify(blocksArr));
buttonStore = blocksArr;
storeEvents.trigger('index-complete')
From here I just needed to setup my event listener to listen for an index-complete
event and then render my inline button only if indexing had finished:
this.storeEventsRef = this.storeEvents.on('index-complete', async () => {
const args = await getButtonById(this.app, id);
if (args) {
ctx.addChild(new InlineButton(codeblock, this.app, args, id))
}
})
To clean up properly, I needed to remove the event listener inside my onunload
:
onunload(): void {
this.storeEvents.offref(this.storeEventsRef);
}
There's one issue with my code. The inline button now renders beautifully on first loading of the vault, but if I navigate away from and back to the note with an inline button...it doesn't render! This is because I'm waiting for 'index-complete' to trigger the rendering. My simple fix was to add a indexComplete
boolean and wrap the whole thing in an if
statement:
if (!this.indexComplete) {
this.storeEventsRef = this.storeEvents.on('index-complete', async () => {
this.indexComplete = true;
const args = await getButtonById(this.app, id);
if (args) {
ctx.addChild(new InlineButton(codeblock, this.app, args, id))
}
})
} else {
const args = await getButtonById(this.app, id);
if (args) {
ctx.addChild(new InlineButton(codeblock, this.app, args, id))
}
}
I'll probably go back and remove the duplication of code here, but it runs and does what I wanted, so good enough. Ship it!
So this is how I created a custom Event and used it to know when it was safe to render my Inline Buttons. What did I do wrong? How could it be better? What is still confusing? Let me know in the OMG discord.