Spaces:
Build error
Inter-Plugin Communication
Three mechanisms are available. Choose based on the relationship between producer and consumer.
| Mechanism | Use when |
|---|---|
dispatchStateChange |
Broadcasting domain state to unknown or multiple consumers. The producer doesn't target a specific plugin. |
Extension points (static extensionPoints) |
A host plugin needs structured contributions from many plugins. Contributions are additive and defined at install/start time. |
getDependency() API |
The interaction targets one specific plugin, or a synchronous return value is required. |
Cross-plugin UI access via the global ui object is never correct. A plugin that needs to affect another plugin's widget must use one of the three mechanisms above.
State propagation
Use dispatchStateChange to broadcast a value that any number of plugins may react to. The producer does not know or care which plugins consume the change.
// Producer: fires when user logs in
async handleLogin(user) {
await this.dispatchStateChange({ user })
}
// Consumer A: reacts to the same state key
async onUserChange(newUser) {
this.updateUserMenu(newUser)
}
// Consumer B: also reacts independently
async onUserChange(newUser) {
if (!newUser) this.clearEditor()
}
Plugin-specific data goes in state.ext to avoid key collisions:
await this.dispatchStateChange({
ext: { [this.name]: { preferences: prefs } }
})
get preferences() {
return this.state?.ext?.[this.name]?.preferences ?? {}
}
Rules:
onStateUpdatehandlers are observers only β callingdispatchStateChangefrom inside one throws an error because state propagation is locked during notification.- Call
dispatchStateChangefrom event handlers, UI callbacks, or top-level async operations. - When
onStateUpdatetriggers async work (e.g. an API call) whose result must be written back to state, usescheduleStateChangeinstead:
async onXmlChange(newXml) {
const permissions = await this.fetchPermissions(newXml);
// scheduleStateChange defers the dispatch until propagation is fully done
await this.scheduleStateChange({ editorReadOnly: !permissions.canEdit });
}
scheduleStateChange is the only legitimate way to write state as a consequence of onStateUpdate. It is not a general escape hatch β synchronous handlers must remain pure observers.
Extension points
Use extension points when a host plugin needs structured contributions from multiple plugins. The host defines the contract; contributors implement it.
Declaring and implementing an extension point
// extension-points.js
export default {
toolbar: {
contentItems: 'toolbar.contentItems',
menuItems: 'toolbar.menuItems',
}
}
// Contributing plugin
import ep from '../extension-points.js'
class MyPlugin extends Plugin {
static extensionPoints = [ep.toolbar.contentItems];
/**
* Extension point handler for `ep.toolbar.contentItems`.
* Called by ToolbarPlugin during start() to collect this plugin's toolbar contributions.
* Delegates to {@link MyPlugin#getToolbarContentItems}.
* @returns {Array<{element: HTMLElement, priority: number, position: string}>}
*/
[ep.toolbar.contentItems](...args) { return this.getToolbarContentItems(...args) }
getToolbarContentItems() {
return [{ element: this.#ui, priority: 5, position: 'center' }]
}
}
The base class discovers the computed method automatically. The key is the full EP path string ("toolbar.contentItems"), so there are no naming conflicts between different namespaces.
ASI hazard: always end
static extensionPoints = [...]with a semicolon. Without it, the parser treats the following[ep.X.Y](...)computed method as a subscript access on the array, causing aSyntaxError: Unexpected token '{'.
Extension point handler methods MUST be documented with JSDoc: state the EP being handled, which host plugin invokes them and when, the Delegates to link, and @param/@returns tags.
Invoking an extension point (host side)
// ToolbarPlugin.start() collects all contributions
const results = await this.context.invokePluginEndpoint(
ep.toolbar.contentItems, [], { result: 'values', throws: false }
)
for (const items of results) {
if (!Array.isArray(items)) continue
for (const { element, priority = 0, position = 'center' } of items) {
ui.toolbar.add(element, priority, position)
}
}
Auto-discovered extension points
The base class auto-mounts these without any declaration:
- Lifecycle methods:
install,ready,start,shutdown,onStateUpdateβ just define the method. - Per-key state handlers:
on<Key>Changeβ follow the naming convention, e.g.onXmlChange,onUserChange.
All other extension points require static extensionPoints with a corresponding computed handler method.
getDependency() API
Use when the interaction is intentionally directed at one specific plugin, or when a synchronous return value is required (state and extension points are both asynchronous/broadcast).
class DocumentActionsPlugin extends Plugin {
// Private getters β resolved lazily at call time, not at construction time.
// This avoids initialization-order issues and circular dependency problems.
get #logger() { return this.getDependency('logger') }
get #xmlEditor() { return this.getDependency('xmleditor') }
get #client() { return this.getDependency('client') }
async saveRevision() {
this.#logger.debug('saving...')
const xmlDoc = this.#xmlEditor.getXmlTree()
await this.#client.saveXml(xmlDoc)
}
}
Only add a plugin to deps when it must be fully installed before this plugin's own install() runs. For plugins only needed at action time (button clicks, async operations), the private getter above is sufficient β no deps entry required.
Check for circular deps: before declaring a dep, verify the full chain does not lead back to the current plugin. If plugin A depends on B and B (transitively) depends on A, declare neither as a dep β use lazy getDependency() calls at call time instead.
Decision guide
Need to affect another plugin's widget?
β No direct ui.otherPlugin.* access allowed.
β If the other plugin owns the widget, add an API method to it and use getDependency().
β If the value is domain state (e.g. readOnly, currentUser), use dispatchStateChange().
Need contributions from many plugins?
β Define an extension point. Contributors implement it passively.
Need a return value from one specific plugin?
β getDependency().methodName()
Broadcasting a domain event?
β dispatchStateChange()