# VoltX Plugin System Spec ## Overview The plugin system enables extending the framework with custom `data-volt-*` attribute bindings. Plugins follow the same binding patterns as core bindings (text, html, class, events) but can implement specialized behaviors like persistence, scrolling, and URL synchronization. ## Design Goals ### Extensibility Plugins can access the full binding context including the DOM element, reactive scope, signal utilities, and cleanup registration. ### Explicit Opt-In Built-in plugins require explicit registration to keep the core bundle minimal. Applications only load the functionality they use. ### Simplicity Plugin API mirrors the internal binding handler signature. Developers who end up familiar with Volt internals can easily create plugins. ### Consistency Plugins should integrate seamlessly with the mount/unmount lifecycle, cleanup system, and reactive primitives. ## Plugin API ### Registration Plugins are registered using the `registerPlugin()` function: ```ts registerPlugin(name: string, handler: PluginHandler): void ``` The plugin name becomes the `data-volt-*` attribute suffix. For example, registering a plugin named `"tooltip"` enables `data-volt-tooltip` attributes. ### Plugin Handler Plugin handlers receive a context object and the attribute value: ```ts type PluginHandler = (context: PluginContext, value: string) => void ``` The handler should: 1. Parse the attribute value 2. Set up bindings and subscriptions 3. Register cleanup functions for unmount ### PluginContext The context object provides: ```ts interface PluginContext { element: Element; // The bound DOM element scope: Scope; // Reactive scope with signals addCleanup(fn: CleanupFunction): void; // Register cleanup findSignal(path: string): Signal | undefined; // Locate signals by path evaluate(expression: string): unknown; // Evaluate expressions } ``` ### Example: Custom Tooltip Plugin ```ts import { registerPlugin } from 'voltx.js'; registerPlugin('tooltip', (context, value) => { const tooltip = document.createElement('div'); tooltip.className = 'tooltip'; tooltip.textContent = context.evaluate(value); const show = () => document.body.appendChild(tooltip); const hide = () => tooltip.remove(); context.element.addEventListener('mouseenter', show); context.element.addEventListener('mouseleave', hide); context.addCleanup(() => { hide(); context.element.removeEventListener('mouseenter', show); context.element.removeEventListener('mouseleave', hide); }); const signal = context.findSignal(value); if (signal) { const unsubscribe = signal.subscribe((newValue) => { tooltip.textContent = String(newValue); }); context.addCleanup(unsubscribe); } }); ``` ## Built-in Plugins VoltX.js ships with three built-in plugins that must be explicitly registered. ### data-volt-persist Synchronizes signal values with persistent storage (`localStorage`, `sessionStorage`, `IndexedDB`). **Syntax:** ```html ``` **Storage Types:** - `local` - localStorage (persistent across sessions) - `session` - sessionStorage (cleared on tab close) - `indexeddb` - IndexedDB (large datasets, async) - Custom adapters via `registerStorageAdapter()` **Behavior:** 1. On mount: Load persisted value into signal (if exists) 2. On signal change: Persist new value to storage 3. On unmount: Clean up storage listeners **Examples:** ```html
``` **Custom Storage Adapters:** ```ts interface StorageAdapter { get(key: string): Promise | unknown; set(key: string, value: unknown): Promise | void; remove(key: string): Promise | void; } registerStorageAdapter('custom', { async get(key) { /* ... */ }, async set(key, value) { /* ... */ }, async remove(key) { /* ... */ } }); ``` ### data-volt-scroll Manages scroll behavior including position restoration, programmatic scrolling, scroll spy, and smooth scrolling. **Syntax:** ```html
``` **Behaviors:** **Position Restoration:** ```html
``` Saves scroll position to the specified signal and restores on mount. **Scroll-To:** ```html
``` Scrolls to element when the specified signal changes to match element's ID or selector. **Scroll Spy:** ```html
``` Updates signal with boolean visibility state using Intersection Observer. **Smooth Scrolling:** ```html
``` Enables smooth scrolling with configurable behavior from signal. ### data-volt-url Synchronizes signal values with URL parameters and hash-based routing. **Syntax:** ```html
``` **Behaviors:** **Read URL Parameters:** ```html
``` Reads URL parameter on mount and sets signal value. Signal changes do not update URL. **Bidirectional Sync:** ```html ``` You can also use the shorthand attribute form where the signal name is encoded in the attribute suffix: ```html ``` Changes to signal update URL parameter, changes to URL update signal. Uses History API for clean URLs. **Hash Routing:** ```html
``` Keeps hash portion of URL in sync with signal. Useful for client-side routing. **Notes:** - Uses History API (`pushState`/`replaceState`) for param sync - Listens to `popstate` for browser back/forward - Debounces URL updates to avoid excessive history entries - Automatically serializes/deserializes values (strings, numbers, booleans) - Accepts `data-volt-url="mode:signal"` or `data-volt-url:signal="mode"` forms - Supports `query`, `hash`, and `history` mode aliases in shorthand attributes (e.g., `data-volt-url:filter="query"`) ## Implementation ### Integration The binder system checks the plugin registry before falling through to unknown attribute warnings ### Context The binder creates a PluginContext from BindingContext: ```ts function createPluginContext(bindingContext: BindingContext): PluginContext { return { element: bindingContext.element, scope: bindingContext.scope, addCleanup: (fn) => bindingContext.cleanups.push(fn), findSignal: (path) => findSignalInScope(bindingContext.scope, path), evaluate: (expr) => evaluate(expr, bindingContext.scope) }; } ``` ### Module Structure ```sh src/ core/ plugin.ts # Plugin registry and API binder.ts # Modified to integrate plugins plugins/ persist.ts # Persistence plugin scroll.ts # Scroll behavior plugin url.ts # URL synchronization plugin index.ts # Exports registerPlugin and built-in plugins ``` ## Bundle Size Considerations With explicit registration, applications control their bundle size: - Core framework: ~15 KB gzipped (no plugins) - Each plugin: ~1-3 KB gzipped - Applications import only what they use - Tree-shaking eliminates unused plugins Example bundle breakdown: ```sh volt/core : 15 KB volt/plugins/persist : 2 KB volt/plugins/scroll : 2.5 KB volt/plugins/url : 1.5 KB -------------------------------- Total (all plugins) : 21 KB ``` ## Extension Points Future plugin capabilities: - Lifecycle hooks (beforeMount, afterMount, beforeUnmount) - Plugin dependencies and composition - Plugin configuration API - Async plugin initialization - Plugin registry