@qalma/editor

July 7, 2026 · View on GitHub

Qalma

@qalma/editor

Angular-first, headless rich text editor toolkit built on ProseMirror.

Qalma gives you a typed editor controller, signal-based state, and a small set of unstyled Angular primitives (<qalma-editor>, <qalma-content>, <qalma-toolbar>, qalmaCommand). Everything else — toolbar UI, styling, menus, popovers — stays in your app. You choose the plugins, you own the markup.

Documentation & live demo → qalma.dev

Status: pre-1.0 (0.x). The public API is stabilizing but may still change before 1.0.

Installation

npm install @qalma/editor

@angular/core >=21 <22 is a peer dependency. @angular/forms is an optional peer dependency used only by the @qalma/editor/forms entrypoint.

Quick start

import { createQalmaEditor, HistoryPlugin, TextFormattingKit } from '@qalma/editor';

const editor = createQalmaEditor({
  content: '<p>Hello world</p>',
  plugins: [
    ...TextFormattingKit,
    HistoryPlugin.configure({
      depth: 200,
      newGroupDelay: 750,
    }),
  ],
});
<qalma-editor [editor]="editor">
  <qalma-toolbar>
    <button qalmaCommand="toggleBold">Bold</button>
    <button qalmaCommand="undo">Undo</button>
    <button qalmaCommand="redo">Redo</button>
  </qalma-toolbar>

  <qalma-content />
</qalma-editor>

Core concepts

createQalmaEditor(options)

Builds a QalmaEditorController, the headless API your components bind to:

MemberDescription
html: Signal<string>Serialized HTML of the current document, kept in sync with every edit.
editable: Signal<boolean>Whether the document can be edited.
execute(command, value?)Runs a registered command, e.g. editor.execute('toggleBold').
canExecute(command, value?)Whether a command would currently succeed.
isCommandActive(command)Whether a toggleable command is active for the current selection.
query<T>(name)Reads plugin-provided state, e.g. the link or image under the cursor.
getCoordinatesAtPosition(pos)Reads viewport coordinates for a document position, or null.
setHtml(html)Replaces the document content from an HTML string.
getJSON()Serializes the document to ProseMirror's native, lossless JSON.
setJSON(doc)Replaces the document content from a QalmaDocument JSON object.
getMarkdown()Serializes the document to Markdown (CommonMark + GFM).
setEditable(editable)Toggles editability at runtime.
focus()Focuses the editor view.

options.content accepts an initial HTML string, and options.plugins accepts the list of QalmaPlugins that define the schema, commands, and behavior available to the editor.

Serializing content

The controller can read and write the document in three formats:

  • HTMLhtml() (a live signal) and setHtml(). Best for rendering and interop with existing HTML content.
  • JSONgetJSON() and setJSON(). ProseMirror's native document model; lossless and the recommended format to persist and restore content.
  • MarkdowngetMarkdown(). CommonMark plus GFM (tables, task lists, strikethrough). Marks Markdown cannot express (underline, text color, highlight, sub/superscript, mentions) fall back to inline HTML so no content is dropped. Markdown is output-only — typing Markdown syntax is handled by the per-plugin input rules.
const doc = editor.getJSON(); // persist this
editor.setJSON(doc); // restore it later, losslessly

const markdown = editor.getMarkdown(); // export to Markdown

Components and directives

  • <qalma-editor [editor]="editor"> — root container that shares the controller with its content.
  • <qalma-content /> — mounts the ProseMirror view. Style its .qalma-content / .ProseMirror descendants from your app's CSS.
  • <qalma-toolbar> — an accessible (role="toolbar") wrapper for your toolbar controls. Purely structural.
  • button[qalmaCommand] — binds a button to a command by name. It calls execute() on click, reflects isCommandActive() via .qalma-command-active and aria-pressed, and disables itself when canExecute() is false. Pass a command argument with [qalmaCommandValue].
  • QalmaControlValueAccessor from @qalma/editor/forms — optional Angular forms adapter for formControl, formControlName, and ngModel.

Plugins and kits

A QalmaPlugin contributes schema nodes/marks, commands, command-state queries, shortcuts, and ProseMirror plugins. A kit (e.g. TextFormattingKit) is just a readonly QalmaPlugin[] bundling related plugins — spread it into plugins like any other entry.

Configurable plugins expose a .configure(options) method that returns a new plugin instance with merged options, e.g. HistoryPlugin.configure({ depth: 200 }).

Available plugins

PluginCommands
BoldPlugin, ItalicPlugin, UnderlinePlugin, StrikePlugin (TextFormattingKit)toggleBold, toggleItalic, toggleUnderline, toggleStrike
InlineCodePlugintoggleInlineCode (Mod-e, single-backtick input rule)
MonospacePlugintoggleMonospace
SubscriptSuperscriptPlugintoggleSubscript, toggleSuperscript
HeadingsPluginsetParagraph, toggleHeading1toggleHeading6 (configurable levels)
BlockquotePlugintoggleBlockquote
TablePlugin from @qalma/editor/tableinsertTable, addRow*/addColumn*, deleteRow/deleteColumn/deleteTable, mergeCells/splitCell, toggleHeader*
HorizontalRulePlugininsertHorizontalRule
ListsPlugintoggleBulletList, toggleOrderedList, splitListItem, liftListItem, sinkListItem
TaskListPlugintoggleTaskList, toggleTaskItemChecked, setTaskItemChecked, splitTaskItem, liftTaskItem, sinkTaskItem
CodeBlockPlugintoggleCodeBlock, setCodeBlockLanguage
LinkPluginsetLink, selectLink, unsetLink
ImagePlugininsertImage, updateImage
MentionPlugininsertMention
SlashCommandPlugindeleteSlashCommand, dismissSlashCommand
DragHandlePluginselectBlock, deleteBlock, duplicateBlock, moveBlockTo, moveBlockUp, moveBlockDown
SelectionPluginNone (query('selection'), qalma-selection-update)
ColorPluginsetTextColor, unsetTextColor, setBackgroundColor, unsetBackgroundColor
HighlightPluginsetHighlight, unsetHighlight
TextAlignPluginalignment commands for configured node types
ClearFormattingPluginclearFormatting
HardBreakPlugininsertHardBreak
HistoryPluginundo, redo (Mod-z, Shift-Mod-z, Mod-y)
PasteRulesPluginnormalizes pasted content
PlaceholderPluginshows placeholder text in an empty document
TrailingParagraphPluginkeeps a trailing empty paragraph at the end of the document

Read each plugin's source under src/lib/plugins for configuration options (e.g. HeadingsPlugin.configure({ levels: [1, 2, 3] }), InlineCodePlugin.configure({ inputRules: false }), MentionPlugin.configure({ trigger: '@' }), SlashCommandPlugin.configure({ trigger: '/' }), LinkPlugin.configure({ allowedProtocols: [...], onClick })).

Learn more

The repository also ships a full example app (apps/sandbox) — a toolbar, link popover, mention menu, image upload, and code block highlighting — built entirely from the public @qalma/editor API. See CONTRIBUTING.md to run it locally.