Recipes
These recipes use only the public @qalma/editor API. They are small on
purpose; compose them into your own Angular components and design system.
Minimal editor
const editor = createQalmaEditor({
content: '<p>Hello world</p>',
plugins: [...TextFormattingKit, HistoryPlugin],
});
<qalma-editor [editor]="editor">
<qalma-toolbar>
<button type="button" qalmaCommand="toggleBold">Bold</button>
<button type="button" qalmaCommand="undo">Undo</button>
<button type="button" qalmaCommand="redo">Redo</button>
</qalma-toolbar>
<qalma-content class="block min-h-40 p-4 [&_.ProseMirror]:outline-none" />
</qalma-editor>
Insert an image from a file input
insertImage(event: Event): void {
const input = event.target;
if (!(input instanceof HTMLInputElement)) {
return;
}
const file = input.files?.item(0);
if (!file || !file.type.startsWith('image/')) {
return;
}
this.editor.execute('insertImage', {
src: `/uploads/${encodeURIComponent(file.name)}`,
alt: file.name.replace(/\.[^.]+$/, ''),
title: file.name,
previewSrc: URL.createObjectURL(file),
});
}
Link toolbar button with a custom popover
readonly href = signal('https://');
openLinkEditor(): void {
const link = this.editor.query<LinkState>('link');
this.href.set(link?.href ?? 'https://');
}
saveLink(): void {
this.editor.execute('setLink', this.href());
}
removeLink(): void {
this.editor.execute('unsetLink');
}
Color swatches
readonly colors = ['rgb(190, 18, 60)', 'rgb(67, 56, 202)'] as const;
readonly textColor = computed(() => this.editor.query<string>('textColor'));
setTextColor(color: string): void {
this.editor.execute('setTextColor', color);
}
@for (color of colors; track color) {
<button type="button" [class.qalma-command-active]="textColor() === color" [disabled]="!editor.canExecute('setTextColor', color)" (mousedown)="$event.preventDefault()" (click)="setTextColor(color)" [style.background-color]="color" [attr.aria-label]="'Set text color ' + color"></button>
}
Slash command pick
pick(option: { command: string; value?: unknown }): void {
if (!this.editor.execute('deleteSlashCommand')) {
return;
}
this.editor.execute('splitSlashCommandBlock');
this.editor.execute(option.command, option.value);
}
Inside lists, the docs playground first splits the list item and lifts it out of the list before running the selected block command. That is app-specific menu policy, not library behavior.
Mention pick
pickUser(user: { id: string; label: string }): void {
this.editor.execute('insertMention', {
id: user.id,
label: user.label,
});
}
Listen to qalma-mention-update and read query<MentionState>('mention') to
position and filter your own menu.
Angular Material toolbar
Qalma commands are ordinary controller calls, so Material stays an app dependency:
<mat-toolbar>
<button mat-icon-button type="button" aria-label="Bold" [class.is-active]="editor.isCommandActive('toggleBold')" [disabled]="!editor.canExecute('toggleBold')" (click)="editor.execute('toggleBold')">
<mat-icon>format_bold</mat-icon>
</button>
</mat-toolbar>
Kendo UI toolbar
<kendo-toolbar aria-label="Editor toolbar">
<kendo-toolbar-button text="Bold" [togglable]="true" [selected]="editor.isCommandActive('toggleBold')" [disabled]="!editor.canExecute('toggleBold')" (click)="editor.execute('toggleBold')" />
</kendo-toolbar>
ng-zorro toolbar
<div role="toolbar" aria-label="Editor toolbar">
<button nz-button type="button" [nzType]="editor.isCommandActive('toggleBold') ? 'primary' : 'default'" [disabled]="!editor.canExecute('toggleBold')" (click)="editor.execute('toggleBold')">
<span nz-icon nzType="bold" aria-hidden="true"></span>
<span class="sr-only">Bold</span>
</button>
</div>
These snippets show the integration boundary, not a dependency promise for
those design systems. Import the components/directives from the versions your
application already uses. If you want Qalma's optional Tailwind-first
components instead, use @qalma/kit.