Search the Qalma docs

Type at least two characters to get started.

Docs

Editor & Controller

createQalmaEditor() returns a QalmaEditorController. The controller is the headless API your Angular components keep and pass into <qalma-editor>.

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

const editor = createQalmaEditor({
  content: '<p>Hello Qalma</p>',
  editable: true,
  plugins: [HeadingsPlugin, ...TextFormattingKit, HistoryPlugin],
});

Options

Option Type Default Description
content string '<p></p>' Initial HTML parsed with the schema produced by your plugins.
editable boolean true Initial read/write state.
plugins readonly QalmaPlugin[] [] Plugins that define the schema, commands, states, queries, shortcuts, and behavior.

The plugin list is copied when the controller is created. Add all required plugins up front; a controller does not expose a method for adding plugins later.

Signals

Member Type Description
html Signal<string> Serialized HTML for the current document. It updates after document-changing transactions.
editable Signal<boolean> Current editability. It updates when you call setEditable().
<section>
  <qalma-editor [editor]="editor">
    <qalma-content class="block min-h-40 p-4 [&_.ProseMirror]:outline-none" />
  </qalma-editor>

  <pre><code>{{ editor.html() }}</code></pre>
</section>

Mounting lifecycle

You normally do not call mount() yourself. <qalma-content> injects the editor context from <qalma-editor>, waits for the browser render pass with afterNextRender, and mounts the ProseMirror view into its internal host.

import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
  QalmaContent,
  QalmaEditor,
  createQalmaEditor,
} from '@qalma/editor';

@Component({
  selector: 'app-document-editor',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [QalmaEditor, QalmaContent],
  template: `
    <qalma-editor [editor]="editor">
      <qalma-content class="block min-h-64 p-4 [&_.ProseMirror]:outline-none" />
    </qalma-editor>
  `,
})
export class DocumentEditor {
  protected readonly editor = createQalmaEditor({
    content: '<p>Draft</p>',
  });
}

If the same controller is mounted into the same host twice, it returns early. If it mounts into a different host, it unmounts the previous view first.

Commands

Commands are registered by plugins. execute() returns true when the command exists, the editor is editable, the view state exists, and the command applies. When a command succeeds, Qalma focuses the editor view.

this.editor.execute('toggleBold');
this.editor.execute('toggleHeading2');
this.editor.execute('setLink', {
  href: 'https://angular.dev',
  target: '_blank',
});

Use canExecute() to decide whether custom controls should be enabled:

readonly canSetLink = computed(() =>
  this.editor.canExecute('setLink', 'https://angular.dev'),
);

Use isCommandActive() for toggle state. It returns false for commands with no command-state query.

readonly boldActive = computed(() =>
  this.editor.isCommandActive('toggleBold'),
);

hasCommandState(commandName) tells you whether a command exposes active state. QalmaCommand uses it to set aria-pressed only for stateful commands.

Queries

Queries are plugin-provided read models for UI that needs more than a boolean.

import { LinkState } from '@qalma/editor';

const link = this.editor.query<LinkState>('link');

if (link) {
  console.log(link.href, link.text, link.from, link.to);
}

query() returns null when the query is not registered or when the query has no value for the current selection. hasQuery(queryName) checks registration.

Position coordinates

getCoordinatesAtPosition(position) returns viewport coordinates for a document position, or null when the editor is not mounted, the position is invalid, or the browser cannot measure it. Use it for consumer-owned overlays such as slash menus without reaching into ProseMirror's EditorView.

Empty state

isEmpty() reports whether the document has no user-visible content — no text and no media (images, mentions, tables, horizontal rules). Empty paragraphs, empty containers, and hard breaks all count as empty.

readonly canSubmit = computed(() => !this.editor.isEmpty());

It is computed from the document model, so it never touches the DOM and is safe to call during server-side rendering. Like the other read models it tracks editor state, so it stays live inside computed and effect. The forms adapter uses it to map an empty editor to an empty control value so Validators.required behaves with rich text.

Updating content and editability

setHtml() replaces the whole document. Before mount it only updates the HTML signal. After mount it rebuilds the editor state with the same schema and plugins.

loadDocument(html: string): void {
  this.editor.setHtml(html);
  this.editor.focus();
}

The controller also reads and writes the document as JSON and Markdown:

Method Description
getJSON() Serializes to ProseMirror's native, lossless JSON (QalmaDocument).
setJSON(doc) Replaces the document from a QalmaDocument. Works before and after mount.
getMarkdown() Serializes to Markdown (CommonMark + GFM); output-only.
import { QalmaDocument } from '@qalma/editor';

const doc: QalmaDocument = this.editor.getJSON(); // persist losslessly
this.editor.setJSON(doc); // restore later

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

See Content & Serialization for the format comparison and how Markdown falls back to inline HTML for marks it cannot express.

setEditable(false) makes command execution and canExecute() return false, and updates the ProseMirror editable prop.

toggleReadOnly(readOnly: boolean): void {
  this.editor.setEditable(!readOnly);
}

focus() focuses the mounted editor view. If the view has not mounted yet, it does nothing.