Custom Events

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!

The Problem

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.

The Janky Solution

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.

Events Class

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;
}

Adding an 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);
  }

One Problem

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!

Conclusion

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.

custom-events
Interactive graph
On this page
Custom Events
The Problem
The Janky Solution
Events Class
Adding an index-complete Event
One Problem
Conclusion