--- type: Design title: Joplin Shopping List Plugin — Design Patterns description: Reusable Joplin plugin design patterns applied in the shopping list plugin. resource: https://github.com/lansidev/joplin-shopping-list tags: [joplin, plugin, design-patterns] timestamp: 2026-06-14T00:00:00Z --- # Overview This document captures the reusable Joplin plugin patterns this project relies on, so they can be reapplied in future plugins. Each entry states the pattern, why it is used, and where to find it in the code. # Editor-view plugin (alternative editor) - **What** — register an alternative editor that replaces the Markdown editor for matching notes via `joplin.views.editors.register(viewId, { onActivationCheck, onSetup })`. - **Why** — gives a full-pane, custom UI for a note while keeping Markdown as the source of truth. - **How** — in `onSetup(handle)`: register `onUpdate`/`onMessage`, then `setHtml` + `addScript` for CSS and the webview bundle. See `registerEditor` in `src/index.ts`. # Marker-based activation - **What** — decide `onActivationCheck` from a marker in the note body (``). - **Why** — explicit, predictable opt-in that survives sync and is invisible when rendered. - **How** — `onActivationCheck` fetches the body with `joplin.data.get(['notes', id], {fields:['body']})` and tests for the marker; a Tools-menu command inserts the marker. See `src/index.ts`. # Rely on Joplin's built-in editor toggle (no custom button) - **What** — let Joplin's built-in "toggle editor" button switch between the Markdown editor and the plugin view, instead of registering a custom toolbar button. - **Why** — Joplin already shows that button automatically whenever an editor plugin is *active* (here: when the note contains the marker). A custom toolbar button duplicates it and, worse, is shown unconditionally — even on notes without the marker, where the view cannot activate. - **How** — drive activation from `onActivationCheck` and do **not** call `joplin.views.toolbarButtons.create`. To open the view programmatically (e.g. right after enabling it on a note) use `joplin.commands.execute('showEditorPlugin')`. See `src/index.ts`. # Terms with auto quantity/unit affixing - **What** — users write only the product term per line (a regex, e.g. `Apfel|Äpfel`, `Kartoffeln?`); `compilePattern` wraps it so an optional leading/trailing quantity — a number plus an optional, user-configurable unit — is matched, while keeping the anchored full-string match. - **Why** — avoids repeating `\d+ … kg …` boilerplate on every line; the recognised units are a separate, user-editable list (global for all stores). - **How** — `compilePattern(term, units)` + `parseUnits` in `src/shopping/config.ts`; units are threaded through `parseConfig(dsl, units)`. Matching itself is unchanged (`regex.test(text)` in `src/shopping/router.ts`). # Pure core + thin webview - **What** — keep all logic (parse, route, sort, serialize, view model) in pure modules (`src/shopping/*`) with no Joplin imports; the webview only renders and emits intents. - **Why** — fast, deterministic unit tests (`__tests__/`) and no duplicated logic between processes. - **How** — `operations.ts` exposes `body → body` transforms; `viewmodel.ts` produces the render payload; `webview.ts` contains no business rules. # Request/response + push messaging - **What** — the webview uses `webviewApi.postMessage(intent)` and awaits the resolved view model; the main process also pushes `{type:'render'}` for external changes. - **Why** — immediate feedback for user actions, plus correctness when the note changes elsewhere. - **How** — `editors.onMessage` returns the new view model and also calls `editors.postMessage` (`handleMessage`/`sendRender` in `src/index.ts`); the webview renders from both (`src/webview.ts`). # Echo-free persistence with `saveNote` - **What** — persist the plugin's own edits with `editors.saveNote(handle, {noteId, body})`. - **Why** — `saveNote` does not fire `onUpdate` for the same editor, preventing a save → update → re-render echo loop. - **How** — `handleMessage` saves via `saveNote` and refreshes the webview directly. External edits still arrive through `onUpdate`. # Structured configuration via a dialog (DSL) - **What** — store the configuration as text in **non-public** String settings (stores + units), edited through a `joplin.views.dialogs` dialog with two textareas and validation. - **Why** — Joplin has no multi-line/textarea setting type, and a single-line String input is unusable for multi-line content — so the raw settings are hidden from the settings screen and all editing goes through the dialog. A line-based DSL avoids JSON/regex escaping and mirrors the note layout. - **How** — `openConfigDialog` (`src/configDialog.ts`) loops until valid and reports errors per field with line numbers via `showMessageBox`; a Tools-menu command opens it and writes both values back with `joplin.settings.setValue`. # Theme-aware webview styling - **What** — style the webview with Joplin's CSS variables (`--joplin-background-color`, `--joplin-color`, `--joplin-divider-color`, `--joplin-font-family`, …). - **Why** — the view follows the user's light/dark theme automatically. - **How** — see `src/style/main.css`. # Reproducible toolchain (Nix) + standard build pipeline - **What** — a Nix dev shell pins Node; the webview is an `extraScripts` entry built by the generator-joplin webpack pipeline. - **Why** — identical environment on macOS/Linux with no global installs; the official build produces a valid `.jpl`. - **How** — `flake.nix` / `shell.nix`; `plugin.config.json` (`extraScripts: ["webview.ts"]`); `npm run dist`. # Citations [1] [JoplinViewsEditors API](https://joplinapp.org/api/references/plugin_api/classes/JoplinViewsEditors.html) [2] [Joplin plugin API reference](https://joplinapp.org/api/references/plugin_api/)