Skip to content

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

FieldRole
componentsComponent classes that may appear in the document (componentName must be unique).
formatters / attributesFormats / attributes available in the editor; pass instances or (textbus) => instance factories for lazy creation.
importsMerge multiple **Module**s into this editor in order.
providersProvider[] from @viewfly/core—register or override implementations in the IoC container (e.g. replacing Adapter requires the correct provide token).
pluginsAfter the main view render completes, run each setup (see Plugin below).
readonly / historyStackSize / zenCoding / additionalAdaptersRead-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 each import module; on name clash, the root wins; if the clash is only between imports, earlier entries in the imports array win.
  • providers: root and each module’s providers merge as root first, then imports order; the same token provided multiple times is usually overridden by later ones (if that surprises you, trim the config or imports order).
  • plugins: root and modules’ plugins are concatenated in order and each setup runs 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.allno 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:

  • BrowserModule supplying Adapter, 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

ts
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()
  },
}
ts
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() {},
}
ts
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 equivalent Adapter + NativeSelectionBridge): render fails with missing NativeSelectionBridge / Adapter.
  • Same-named component override ignored: check components merge order (root vs imports), and that componentName matches serialized data.
  • providers override wrong: check whether a later Module overrides the same provide; reorder imports or move the binding to the last-loaded module.
  • Forgot destroy(): listeners and setup teardowns may not run → leaks or double-mount issues.

What’s next

MIT Licensed