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 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.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