Spaces:
Build error
Object-Based Plugin Pattern
The frontend plugin system is built on a primitive object-based pattern. A plugin is a plain JavaScript object with a name and methods placed at extension point paths. The Plugin base class is implemented on top of this pattern: it auto-mounts class methods as extension points at registration time, translating between "class method" and "object property at a path".
This document describes the object-based pattern directly — useful when:
- Reading or modifying existing object-based plugins
- Understanding how the class-based system works internally
- Writing a simple plugin that doesn't need the class infrastructure
For the class-based API, see Frontend Plugin System.
Plugin Descriptor Shape
An object-based plugin is a plain JavaScript object with the following fields:
const plugin = {
// Required
name: 'my-plugin', // Unique string identifier
// Optional
deps: ['config', 'dialog'], // Names of plugins this one depends on (load order)
api: { ... }, // Public API returned by getDependency('my-plugin')
// Lifecycle methods
install, // async (initialState) => void
start, // async () => void
shutdown, // async () => void
// State endpoint
onStateUpdate, // async (changedKeys, state) => void
};
All fields except name are optional.
Lifecycle Methods
install(initialState)
Called during application initialization, in dependency order. Use this to set up UI, register templates, and attach event listeners.
async function install(initialState) {
currentState = initialState;
await registerTemplate('my-template', 'my-template.html');
const el = createSingleFromTemplate('my-template');
document.body.appendChild(el);
}
start()
Called after all plugins have been installed. Use for operations that require other plugins to be ready.
async function start() {
const configApi = getDependency('config');
// configApi is available now
}
shutdown()
Called on window.beforeunload. Use to clean up resources.
async function shutdown() {
// cleanup
}
State Update Handler
async function onStateUpdate(changedKeys, state) {
// changedKeys: string[] of state property names that changed
// state: the new ApplicationState (immutable, do not mutate)
}
State tracking must be done manually. Store the current state in a module-level closure variable and update it in onStateUpdate:
let currentState;
async function onStateUpdate(changedKeys, state) {
currentState = state;
if (changedKeys.includes('user')) {
updateUserUI(state.user);
}
}
Use currentState in event handlers — never capture state at install time, as it will become stale:
async function handleButtonClick() {
// currentState is always up to date
await updateState({ someKey: computeValue(currentState) });
}
Extension Point Path Mapping
Extension point paths map to nested properties on the plugin object. For example:
| Path | Property accessed |
|---|---|
onStateUpdate |
plugin.onStateUpdate |
state.update |
plugin.state.update |
custom.action |
plugin.custom.action |
The PluginManager resolves paths by splitting on . and traversing the object tree. So a plugin can expose multiple endpoints by nesting them:
const plugin = {
name: 'validation',
validation: {
validate: async (content) => { /* ... */ },
configure: async (options) => { /* ... */ }
}
};
These are then invokable as manager.invoke('validation.validate', ...).
The api Field and getDependency()
When another plugin calls getDependency('my-plugin'), it receives the value of plugin.api. This is how object-based plugins expose their public API:
// In my-plugin.js
const api = {
open,
close,
info,
error
};
const plugin = {
name: 'my-plugin',
deps: ['config'],
api,
install,
onStateUpdate
};
export { api, plugin };
export default plugin;
// In another plugin
const myPluginApi = getDependency('my-plugin');
myPluginApi.open({ title: 'Hello' });
If api is omitted, getDependency() returns undefined for that plugin.
For class-based plugins, getDependency() returns whatever getApi() returns on the class instance.
State Updates
Object-based plugins dispatch state changes using the exported updateState function from app.js:
import { updateState } from '../app.js';
async function handleSave() {
await updateState({ xml: { content: newContent, dirty: false } });
}
Never call updateState() inside onStateUpdate() — this creates infinite loops.
Complete Example
// app/src/plugins/my-plugin.js
import { updateState } from '../app.js';
import { registerTemplate, createSingleFromTemplate } from '../ui.js';
let currentState;
let buttonEl;
async function install(initialState) {
currentState = initialState;
await registerTemplate('my-plugin', 'my-plugin.html');
buttonEl = createSingleFromTemplate('my-plugin');
document.body.appendChild(buttonEl);
buttonEl.addEventListener('click', handleClick);
}
async function start() {
// All plugins are installed — safe to use getDependency here
}
async function shutdown() {
buttonEl.removeEventListener('click', handleClick);
}
async function onStateUpdate(changedKeys, state) {
currentState = state;
if (changedKeys.includes('user')) {
buttonEl.disabled = !state.user;
}
}
async function handleClick() {
await updateState({
ext: { 'my-plugin': { lastClicked: Date.now() } }
});
}
const api = {
handleClick
};
const plugin = {
name: 'my-plugin',
deps: ['config'],
api,
install,
start,
shutdown,
onStateUpdate
};
export { api, plugin };
export default plugin;
Registration in app/src/plugins.js:
import myPlugin from './plugins/my-plugin.js';
const plugins = [
// ...
myPlugin,
// ...
];
Choosing Between Object-Based and Class-Based
Use the class-based pattern for new plugins. It provides:
- Automatic
this.statetracking this.dispatchStateChange()instead of the globalupdateState()getDependency()method onthis- Singleton access via
MyPlugin.getInstance() - Auto-discovered
on<Key>Change()handlers
Use the object-based pattern when:
- Modifying an existing object-based plugin where conversion is not warranted
- Writing a very small plugin where the class overhead is unnecessary
- Working at the PluginManager level and needing to understand how endpoints resolve
Related Documentation
- Frontend Plugin System - Full architecture with class-based examples
- Plugin Development Guide - Practical development guide