Rich text editors
Bind an existing editor to a richText() field. Text uses native character operations, formatting uses attribute registers, structure uses hidden anchors. The core has no editor dependency.
The field
import { definePulse, object, richText } from "pulsify-core";
export const Note = definePulse("Note", object({
title: "string",
body: richText(),
})).materialize();
The field supplies the initial document. A plain string becomes a paragraph; later bindings restore the full structure. Each document records its editor family, so a Lexical binding on a ProseMirror document fails before changing anything.
Bind
// bun add pulsify-prosemirror prosemirror-model prosemirror-state prosemirror-transform prosemirror-view
import { bindProseMirror, proseMirrorDocument } from "pulsify-prosemirror";
import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "prosemirror-schema-basic";
import { history } from "prosemirror-history";
const view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({ schema, plugins: [history()] }),
});
const binding = bindProseMirror(view, note.body, {
onError: (error) => console.error(error),
});
// Regular transactions flow into the field and to every peer.
view.dispatch(view.state.tr.insertText("Hi"));
proseMirrorDocument(note.body, schema); // full node tree with marks
// On unmount:
binding.destroy();
view.destroy();
Rules of the road
- Do not add the editor's own collaboration or history plugin to a bound editor. Lexical's binding provides undo and redo; ProseMirror keeps its local history plugin and remote changes bypass it.
- Use the same editor schema and registered node classes on every peer.
field.textand snapshots are a plain-text projection. Read nodes withproseMirrorDocumentorlexicalDocument.- Always call
binding.destroy()on unmount. Switching documents needs a fresh editor state.