<sup>Date: October 10, 2024</sup>
Through various work experiences, I've had the benefit of being able to contribute to existing JavaScript/TypeScript projects such as browser extensions. During my Computer Science education, I didn't get a lot of exposure to web or front end development. In fact, front end work used to scare me too (see: [[Regular expressions used to scare me]]). I really saw myself as a back end first developer. However, taking an existing webpage and using plugins and browser APIs to manipulate the DOM helped things click for me.
In cybersecurity, whether it’s incident response, malware analysis, or otherwise, note-taking is essential. For me, writing things down helps my thought process and keeps track of things I’ve investigated. This led me to go down the path of developing plugins for Obsidian. One of the features I've built extracts IOCs (like IP addresses and file hashes) from the active note. When I got to a point at which JS/TS DOM manipulation was unwieldy, I decided to pivot to using Svelte for this feature.
## Adding Svelte to an Obsidian plugin
Obsidian's developer documentation provides some [basic instructions](https://docs.obsidian.md/Plugins/Getting+started/Use+Svelte+in+your+plugin) for getting started with Svelte in a plugin codebase. Once Svelte is installed and configured in the project, we can mount Svelte components within Obsidian views using [Svelte’s component API](https://svelte.dev/docs/client-side-component-api).
We’ll mount a single component and pass any necessary data down as props. Right now, the sidebar doesn't need to manipulate any plugin data, so I'm not using a store or passing events.
## The old way - TypeScript
Here is a simplified example from my previous pure TypeScript implementation - these are functions from my Sidebar class which extends Obsidian's [`ItemView`](https://docs.obsidian.md/Reference/TypeScript+API/ItemView) class.
```ts
protected async onOpen(): Promise<void> {
const container = this.containerEl.children[1];
container.empty();
container.createEl("h4", {text: this.sidebarTitle});
this.ipEl = this.addContainer(container, "IPs");
this.domainEl = this.addContainer(container, "Domains");
this.hashEl = this.addContainer(container, "Hashes");
const containers = document.getElementsByClassName(this.sidebarContainerClass);
openDetails(containers as HTMLCollectionOf<HTMLDetailsElement>);
const file = this.app.workspace.getActiveFile();
if (file) await this.updateView(file);
}
addContainer(el: Element, text: string) {
const container = el.createEl("details", {cls: this.sidebarContainerClass});
container.createEl("summary", {cls: "tree-item-inner", text: text});
return container.createDiv({cls: "tree-item-children"});
}
```
Even with breaking up the code using the `addContainer` function, I'm not a fan of all of the `createEl` and `createDiv` calls. And while this is reusable enough for my purposes, Svelte's concept of modular components seemed like a much better way to make my Sidebar code scalable if I want to add more features in the future.
## The new way - Svelte
I initially split the interface into four components - the overarching `Sidebar` which will be mounted to the Obsidian view, `IocList`, `Item`, and `Button`. I'll grab a couple of simplified examples to spare pasting all of my code, but it can all be found in the [GitHub repository](https://github.com/acgabbert/obsidian-svelte-components).
The [`Sidebar`](https://github.com/acgabbert/obsidian-svelte-components/blob/main/src/lib/components/Sidebar.svelte) component takes an array of [`ParsedIndicators`](https://github.com/acgabbert/obsidian-utils/blob/main/src/sidebar.ts) objects and loops over them to display them, and pass the data down to an `IocList` component.
```Svelte
<!-- Sidebar.svelte -->
<h4>Extracted Indicators</h4>
{#each indicators as indicatorList}
{#if indicatorList.items.length > 0}
<IocList indicatorList={indicatorList}/>
{/if}
{/each}
```
The `IocList` component leverages properties from the props in order to display the data.
```Svelte
<!-- IocList.svelte -->
<details class="sidebar-container tree-item" {open}>
<summary class="tree-item-inner">{indicatorList.title}</summary>
<div class="tree-item-children">
{#each indicatorList.items as item}
<Item item={item} buttons={indicatorList.sites}/>
{/each}
</div>
{#if indicatorList.sites}
<div class="table-container">
<table>
<tr class="sidebar-table-row">
{#each indicatorList.sites as site}
{#if site.multisearch && multisearchLinks.has(site.shortName)}
<Button
href={getMultisearchLink(site.shortName)}
title={`Search all - ${site.shortName}`}
/>
{/if}
{/each}
</tr>
</table>
</div>
{/if}
</details>
```
I find this visual representation is much nicer to deal with, when compared with creating elements via TypeScript. It also greatly simplifies my TypeScript code when mounting the component:
```ts
protected async onOpen(): Promise<void> {
if (!this.plugin) return;
const file = this.plugin.app.workspace.getActiveFile();
if (file) {
await this.parseIndicators(file);
if (this.iocs) {
this.sidebar = new Sidebar({
target: this.contentEl,
props: {
indicators: this.iocs
}
});
}
}
}
```
## Conclusion
Developing this small project in Svelte not only greatly simplified my TypeScript code and enabled easier changes in the future, but it gave me more of an appreciation for HTML and the DOM. Having this Svelte experience has also allowed me to rethink other aspects of Obsidian plugin development that can be unwieldy, such as dealing with a large number of boolean settings.
Additionally, I've published some of the code I use for plugin development in NPM packages. Since this is mostly leveraged in personal projects, it might not be useful to anyone else - but it's out there just in case!
- `obsidian-cyber-utils`: [GitHub](https://github.com/acgabbert/obsidian-utils), [NPM](https://www.npmjs.com/package/obsidian-cyber-utils)
- `obsidian-svelte-components`: [GitHub](https://github.com/acgabbert/obsidian-svelte-components), [NPM](https://www.npmjs.com/package/obsidian-svelte-components)