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.