Modules & extensions
Once you know Concepts, Component basics, and the Browser module, this page explains how BrowserModule, collaboration, custom providers, and more fit into one Textbus instance: Module / imports / Plugin responsibilities, startup and teardown order, and practical rules for Registry name clashes and providers overrides.
If you only need more block types, formats, or attributes, start with the getting-started material and Advanced components; here the focus is config merging and extension timing.
TextbusConfig and Module
TextbusConfig (the type of new Textbus({ ... }) args) extends Module: the root config may itself carry components / formatters / attributes / providers / plugins plus Module lifecycle hooks. imports?: Module[] merges packaged modules such as BrowserModule and CollaborateModule into the same editor instance.
Module is a plain object (class instance or literal)—“a bundle of registrations + optional hooks”; you do not have to extend a base class.
Common config fields
| Field | Role |
|---|---|
components | Component classes that may appear in the document (componentName must be unique). |
formatters / attributes | Formats / attributes available in the editor; pass instances or (textbus) => instance factories for lazy creation. |
imports | Merge multiple **Module**s into this editor in order. |
providers | Provider[] from @viewfly/core—register or override implementations in the IoC container (e.g. replacing Adapter requires the correct provide token). |
plugins | After the main view render completes, run each setup (see Plugin below). |
readonly / historyStackSize / zenCoding / additionalAdapters | Read-only, history stack, Zen Coding sugar, extra **Adapter**s, and other globals. |
In the browser you must end up with a usable Adapter and NativeSelectionBridge (usually from BrowserModule); otherwise render fails.
imports and list merging
Rough rules (components / formats / attributes behave differently from providers / plugins):
components/formatters/attributes: items from the root config merge before eachimportmodule; on name clash, the root wins; if the clash is only betweenimports, earlier entries in theimportsarray win.providers: root and each module’sprovidersmerge as root first, thenimportsorder; the same token provided multiple times is usually overridden by later ones (if that surprises you, trim the config orimportsorder).plugins: root and modules’pluginsare concatenated in order and eachsetupruns after the main view is ready.
When formatters / attributes use (textbus) => instance, factories run after binding to the current editor so the final instance list is resolved.
Module lifecycle (order)
beforeEach
Runs when creating the editor: first each imported module’s beforeEach in imports order, then root config.beforeEach if present. Use for light prep before registration.
setup
**await**ed during render: each module’s setup in imports order, then root config.setup. May return a teardown function (or a Promise that resolves to one); destroy() runs these to release resources you attached in setup.
Multiple setup hooks are waited with Promise.all—no guaranteed order among them (only that all finish before later startup steps).
onAfterStartup
After initialization finishes and the main view is ready: first imports order, then root onAfterStartup. Use when you need DOM present or the edit loop running (auto-focus, analytics, …).
onDestroy
Inside textbus.destroy(): root config and plugins onDestroy first, then import modules, then setup teardowns, then document view and kernel services shut down. In onDestroy, do not assume plugins still work; release custom resources outside-in.
Always call destroy() on page unload to avoid leaking input handlers and subscriptions.
Plugin and Module.plugins
Plugin only has setup(textbus) and optional onDestroy()—no components / providers, etc.
When it runs: Plugin.setup runs after the main Adapter has rendered—after all Module.setup hooks. Use for extensions that only need ready DOM / mounted views (toolbars, debug overlays). Division of labor: Module registers model + platform wiring and shell; Plugin attaches UI or side effects after the view is ready.
Registry and name resolution
textbus.get(Registry) resolves literals by componentName, format name, and attribute name into components or slots. Which registration wins follows the merge order for components / formatters / attributes above: to override a built-in block or format, put your class or instance on the new Textbus root config, or place the Module earlier in imports (when only reordering imports).
providers customization and overrides
providers matches Provider from @viewfly/core used by @textbus/core (provide / useClass / useFactory / useValue / deps, …). Used for:
BrowserModulesupplyingAdapter,NativeSelectionBridge, …;- Collaboration (or other modules) replacing tokens such as
History; - App code registering
MessageBus,CustomUndoManagerConfig, … (see Collaboration).
When overriding, provide must match the target token exactly; when unsure, follow types or the provide patterns in topic docs like Collaboration.
Example: custom Module and Plugin
import type { Module, Textbus } from '@textbus/core'
export const featureModule: Module = {
setup(textbus: Textbus) {
const sub = textbus.onReady.subscribe(() => {
// Safe to touch DOM / Commander after ready
})
return () => sub.unsubscribe()
},
}import type { Plugin, Textbus } from '@textbus/core'
export const toolbarPlugin: Plugin = {
setup(textbus: Textbus) {
// Main view mounted; safe for querySelector, external UI
void textbus
},
onDestroy() {},
}import { Textbus } from '@textbus/core'
import { featureModule } from './feature-module'
import { toolbarPlugin } from './plugin-toolbar'
const editor = new Textbus({
imports: [featureModule],
plugins: [toolbarPlugin],
})In a real app you still merge BrowserModule (or supply Adapter + NativeSelectionBridge yourself)—see Browser module.
Troubleshooting
- No
BrowserModule(or equivalentAdapter+NativeSelectionBridge):renderfails with missingNativeSelectionBridge/Adapter. - Same-named component override ignored: check
componentsmerge order (root vsimports), and thatcomponentNamematches serialized data. providersoverride wrong: check whether a laterModuleoverrides the sameprovide; reorderimportsor move the binding to the last-loaded module.- Forgot
destroy(): listeners andsetupteardowns may not run → leaks or double-mount issues.
What’s next
- Selection & commands: Selection, Query & operations
- Browser integration: Browser module
- Collaboration &
providers: Collaboration
