Skip to content

Angular

The @domternal/angular package provides five standalone components that wrap the headless editor with Angular-native APIs. All components use signals, OnPush change detection, and modern Angular 17.1+ features.

Click to try it out
Terminal window
pnpm add @domternal/core @domternal/theme @domternal/angular

Add the theme to your global stylesheet:

styles.scss
@use '@domternal/theme';
import { Component, signal } from '@angular/core';
import {
DomternalEditorComponent,
DomternalToolbarComponent,
DomternalBubbleMenuComponent,
} from '@domternal/angular';
import { Editor, StarterKit, BubbleMenu } from '@domternal/core';
@Component({
selector: 'app-editor',
imports: [DomternalEditorComponent, DomternalToolbarComponent, DomternalBubbleMenuComponent],
templateUrl: './editor.html',
})
export class EditorComponent {
editor = signal<Editor | null>(null);
extensions = [StarterKit, BubbleMenu];
content = '<p>Hello from Angular!</p>';
}
editor.html
@if (editor(); as ed) {
<domternal-toolbar [editor]="ed" />
}
<domternal-editor
[extensions]="extensions"
[content]="content"
(editorCreated)="editor.set($event)"
/>
@if (editor(); as ed) {
<domternal-bubble-menu [editor]="ed" />
}

The toolbar and bubble menu auto-render buttons based on extensions. No manual button wiring needed.

The core editor component. Wraps ProseMirror with Angular inputs/outputs and implements ControlValueAccessor for form integration.

Selector: domternal-editor

Host class: dm-editor

InputTypeDefaultDescription
extensionsAnyExtension[][]Extensions to load. If empty, uses [Document, Paragraph, Text, BaseKeymap, History].
contentContent''Initial content (HTML string or JSON). Reactive: changing this input updates the editor.
editablebooleantrueWhether the editor is editable. Reactive.
autofocusFocusPositionfalseFocus on mount: true, 'start', 'end', 'all', or a position number.
outputFormat'html' | 'json''html'Format for value changes (affects ControlValueAccessor and contentUpdated).
OutputTypeDescription
editorCreatedEditorEmitted when the editor instance is created. Use this to pass the editor to toolbar and bubble menu.
contentUpdated{ editor: Editor }Emitted when the document content changes.
selectionChanged{ editor: Editor }Emitted when the selection changes (without content change).
focusChanged{ editor: Editor; event: FocusEvent }Emitted when the editor receives focus.
blurChanged{ editor: Editor; event: FocusEvent }Emitted when the editor loses focus.
editorDestroyedvoidEmitted when the editor is destroyed.
SignalTypeDescription
htmlContentstringCurrent document as HTML.
jsonContentJSONContent | nullCurrent document as JSON.
isEmptybooleanWhether the document is empty.
isFocusedbooleanWhether the editor has focus.
isEditablebooleanWhether the editor is editable.
// Via output event
editor = signal<Editor | null>(null);
// In template
(editorCreated)="editor.set($event)"
// Or via ViewChild
@ViewChild(DomternalEditorComponent) editorComponent!: DomternalEditorComponent;
// Then: this.editorComponent.editor

Auto-renders toolbar buttons, dropdowns, separators, and keyboard navigation based on the editor’s extensions.

Selector: domternal-toolbar

Host class: dm-toolbar

Host attributes: role="toolbar", aria-label="Editor formatting"

InputTypeDefaultDescription
editorEditorrequiredThe editor instance.
iconsIconSet | nullnullCustom icon set (map of icon name to SVG string). Falls back to built-in Phosphor icons.
layoutToolbarLayoutEntry[]-Custom layout to reorder, group, or filter toolbar items.

By default, the toolbar renders all items from extensions grouped by their group property. Use layout to customize:

// Show only specific items in order
layout = ['bold', 'italic', 'underline', '|', 'heading', 'bulletList', 'orderedList'];

The '|' string inserts a visual separator.

The toolbar supports full keyboard navigation:

KeyAction
ArrowRightFocus next button
ArrowLeftFocus previous button
HomeFocus first button
EndFocus last button
EscapeClose open dropdown

An inline formatting toolbar that appears when the user selects text. Renders buttons based on the editor’s extensions.

Selector: domternal-bubble-menu

InputTypeDefaultDescription
editorEditorrequiredThe editor instance.
itemsstring[]-Fixed list of item names to show (e.g., ['bold', 'italic', 'code']).
contextsRecord<string, string[] | true | null>-Context-aware items. Key is a node type name or 'text'/'table'. Value is item names, true for all valid items, or null to hide the menu.
shouldShowfunction-Custom predicate (props) => boolean to control visibility.
placement'top' | 'bottom''top'Menu placement relative to the selection.
offsetnumber8Pixel offset from the selection.
updateDelaynumber0Delay in milliseconds before updating position.

Add custom buttons via <ng-content>:

<domternal-bubble-menu [editor]="ed">
<button (click)="customAction()">Custom</button>
</domternal-bubble-menu>

Show different buttons depending on what the user selected:

<domternal-bubble-menu
[editor]="ed"
[contexts]="{
text: ['bold', 'italic', 'underline', '|', 'link'],
heading: ['bold', 'italic', '|', 'link'],
codeBlock: null,
image: ['imageFloatLeft', 'imageFloatCenter', 'imageFloatRight', '|', 'deleteImage']
}"
/>
  • text - Default for text selections
  • null - Hide the menu in that context
  • true - Show all valid items for that context

A menu that appears on empty lines, allowing users to insert block-level content.

Selector: domternal-floating-menu

InputTypeDefaultDescription
editorEditorrequiredThe editor instance.
shouldShowfunction-Custom predicate to control visibility. Default: shows on empty paragraphs.
offsetnumber0Pixel offset from the cursor position.

The floating menu renders whatever you put inside it:

<domternal-floating-menu [editor]="ed">
<button (click)="ed.commands.setHeading({ level: 1 })">H1</button>
<button (click)="ed.commands.toggleBulletList()">List</button>
<button (click)="ed.commands.setHorizontalRule()">Divider</button>
</domternal-floating-menu>

A searchable emoji picker panel that opens from the toolbar emoji button.

Selector: domternal-emoji-picker

InputTypeDefaultDescription
editorEditorrequiredThe editor instance.
emojisEmojiPickerItem[]requiredArray of emoji definitions with emoji, name, and group properties.
import { DomternalEmojiPickerComponent } from '@domternal/angular';
import { Emoji, emojis } from '@domternal/extension-emoji';
extensions = [StarterKit, Emoji];
emojiData = emojis;
<domternal-emoji-picker [editor]="ed" [emojis]="emojiData" />

The picker opens when the user clicks the emoji toolbar button. It includes search, category tabs, frequently used section, and smooth scrolling.

The editor component implements ControlValueAccessor, so it works with ngModel and reactive forms.

@Component({
imports: [FormsModule, DomternalEditorComponent],
template: `
<domternal-editor
[extensions]="extensions"
[(ngModel)]="content"
/>
<pre>{{ content }}</pre>
`,
})
export class MyComponent {
extensions = [StarterKit];
content = '<p>Initial content</p>';
}
@Component({
imports: [ReactiveFormsModule, DomternalEditorComponent],
template: `
<domternal-editor
[extensions]="extensions"
[formControl]="editorControl"
/>
`,
})
export class MyComponent {
extensions = [StarterKit];
editorControl = new FormControl('<p>Initial content</p>');
}

By default, the form value is HTML. Set outputFormat="json" to get JSON instead:

<domternal-editor
[extensions]="extensions"
[(ngModel)]="content"
outputFormat="json"
/>

Reactive forms disable() and enable() automatically toggle the editor’s editable state:

this.editorControl.disable(); // Editor becomes read-only
this.editorControl.enable(); // Editor becomes editable

All components use Angular signals for state management. The recommended pattern:

@Component({
imports: [DomternalEditorComponent, DomternalToolbarComponent],
template: `
@if (editor(); as ed) {
<domternal-toolbar [editor]="ed" />
}
<domternal-editor
[extensions]="extensions"
(editorCreated)="editor.set($event)"
/>
<p>{{ isEmpty() ? 'Empty' : 'Has content' }}</p>
`,
})
export class MyComponent {
editor = signal<Editor | null>(null);
extensions = [StarterKit];
// Derived state from editor signals
isEmpty = computed(() => {
const ed = this.editor();
return ed ? ed.isEmpty : true;
});
}
  • No manual subscription management (no subscribe()/unsubscribe())
  • Works with OnPush change detection out of the box
  • Compatible with zoneless Angular
  • Editor events update signals automatically via NgZone.run()

Apply the theme class to a parent element:

<!-- Always dark -->
<div class="dm-theme-dark">
<domternal-toolbar [editor]="ed" />
<domternal-editor [extensions]="extensions" (editorCreated)="editor.set($event)" />
</div>
<!-- Follow system preference -->
<div class="dm-theme-auto">
<domternal-toolbar [editor]="ed" />
<domternal-editor [extensions]="extensions" (editorCreated)="editor.set($event)" />
</div>

Toggle at runtime:

toggleTheme() {
document.body.classList.toggle('dm-theme-dark');
}

Replace the built-in Phosphor icons with your own:

const customIcons: IconSet = {
bold: '<svg>...</svg>',
italic: '<svg>...</svg>',
// ... other icon names
};
<domternal-toolbar [editor]="ed" [icons]="customIcons" />

Any icon not in your custom set falls back to the built-in icon.

ProseMirror events fire outside Angular’s zone. The editor component handles this internally by wrapping transaction handlers in NgZone.run(). You don’t need to manage this yourself.

If you listen to editor events directly (via editor.on()), wrap your handler:

private ngZone = inject(NgZone);
ngAfterViewInit() {
this.editor()?.on('update', ({ editor }) => {
this.ngZone.run(() => {
// Update Angular state here
this.mySignal.set(editor.getHTML());
});
});
}

All components are standalone (no NgModule required). Import them directly:

import {
DomternalEditorComponent,
DomternalToolbarComponent,
DomternalBubbleMenuComponent,
DomternalFloatingMenuComponent,
DomternalEmojiPickerComponent,
} from '@domternal/angular';

Re-exported types from @domternal/core for convenience:

import type { Content, AnyExtension, FocusPosition, JSONContent } from '@domternal/angular';
import { Editor } from '@domternal/angular';

The EmojiPickerItem type is also exported:

import type { EmojiPickerItem } from '@domternal/angular';

Also exported:

import { DEFAULT_EXTENSIONS } from '@domternal/angular';
// [Document, Paragraph, Text, BaseKeymap, History]
  • Angular 17.1.0 or later
  • @domternal/core 0.2.0 or later
  • All components use ChangeDetectionStrategy.OnPush and ViewEncapsulation.None