pdf-tei-editor / docs /development /plugin-system-object-based.md
cmboulanger's picture
disaster-recovery deploy of pdf-tei-editor
6a49f21 verified
|
Raw
History Blame Contribute Delete
6.95 kB
# 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-system-frontend.md).
## Plugin Descriptor Shape
An object-based plugin is a plain JavaScript object with the following fields:
```javascript
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.
```javascript
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.
```javascript
async function start() {
const configApi = getDependency('config');
// configApi is available now
}
```
### `shutdown()`
Called on `window.beforeunload`. Use to clean up resources.
```javascript
async function shutdown() {
// cleanup
}
```
## State Update Handler
```javascript
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`:
```javascript
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:
```javascript
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:
```javascript
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:
```javascript
// 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;
```
```javascript
// 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`:
```javascript
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
```javascript
// 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`:
```javascript
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.state` tracking
- `this.dispatchStateChange()` instead of the global `updateState()`
- `getDependency()` method on `this`
- 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](plugin-system-frontend.md) - Full architecture with class-based examples
- [Plugin Development Guide](../code-assistant/plugin-development.md) - Practical development guide