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.
Installation
Section titled “Installation”pnpm add @domternal/core @domternal/theme @domternal/angularAdd the theme to your global stylesheet:
@use '@domternal/theme';Quick start
Section titled “Quick start”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>';}@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.
Components
Section titled “Components”domternal-editor
Section titled “domternal-editor”The core editor component. Wraps ProseMirror with Angular inputs/outputs and implements ControlValueAccessor for form integration.
Selector: domternal-editor
Host class: dm-editor
Inputs
Section titled “Inputs”| Input | Type | Default | Description |
|---|---|---|---|
extensions | AnyExtension[] | [] | Extensions to load. If empty, uses [Document, Paragraph, Text, BaseKeymap, History]. |
content | Content | '' | Initial content (HTML string or JSON). Reactive: changing this input updates the editor. |
editable | boolean | true | Whether the editor is editable. Reactive. |
autofocus | FocusPosition | false | Focus on mount: true, 'start', 'end', 'all', or a position number. |
outputFormat | 'html' | 'json' | 'html' | Format for value changes (affects ControlValueAccessor and contentUpdated). |
Outputs
Section titled “Outputs”| Output | Type | Description |
|---|---|---|
editorCreated | Editor | Emitted 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. |
editorDestroyed | void | Emitted when the editor is destroyed. |
Public signals
Section titled “Public signals”| Signal | Type | Description |
|---|---|---|
htmlContent | string | Current document as HTML. |
jsonContent | JSONContent | null | Current document as JSON. |
isEmpty | boolean | Whether the document is empty. |
isFocused | boolean | Whether the editor has focus. |
isEditable | boolean | Whether the editor is editable. |
Accessing the editor instance
Section titled “Accessing the editor instance”// Via output eventeditor = signal<Editor | null>(null);
// In template(editorCreated)="editor.set($event)"
// Or via ViewChild@ViewChild(DomternalEditorComponent) editorComponent!: DomternalEditorComponent;// Then: this.editorComponent.editordomternal-toolbar
Section titled “domternal-toolbar”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"
Inputs
Section titled “Inputs”| Input | Type | Default | Description |
|---|---|---|---|
editor | Editor | required | The editor instance. |
icons | IconSet | null | null | Custom icon set (map of icon name to SVG string). Falls back to built-in Phosphor icons. |
layout | ToolbarLayoutEntry[] | - | Custom layout to reorder, group, or filter toolbar items. |
Layout
Section titled “Layout”By default, the toolbar renders all items from extensions grouped by their group property. Use layout to customize:
// Show only specific items in orderlayout = ['bold', 'italic', 'underline', '|', 'heading', 'bulletList', 'orderedList'];The '|' string inserts a visual separator.
Keyboard navigation
Section titled “Keyboard navigation”The toolbar supports full keyboard navigation:
| Key | Action |
|---|---|
ArrowRight | Focus next button |
ArrowLeft | Focus previous button |
Home | Focus first button |
End | Focus last button |
Escape | Close open dropdown |
domternal-bubble-menu
Section titled “domternal-bubble-menu”An inline formatting toolbar that appears when the user selects text. Renders buttons based on the editor’s extensions.
Selector: domternal-bubble-menu
Inputs
Section titled “Inputs”| Input | Type | Default | Description |
|---|---|---|---|
editor | Editor | required | The editor instance. |
items | string[] | - | Fixed list of item names to show (e.g., ['bold', 'italic', 'code']). |
contexts | Record<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. |
shouldShow | function | - | Custom predicate (props) => boolean to control visibility. |
placement | 'top' | 'bottom' | 'top' | Menu placement relative to the selection. |
offset | number | 8 | Pixel offset from the selection. |
updateDelay | number | 0 | Delay in milliseconds before updating position. |
Content projection
Section titled “Content projection”Add custom buttons via <ng-content>:
<domternal-bubble-menu [editor]="ed"> <button (click)="customAction()">Custom</button></domternal-bubble-menu>Context-aware menus
Section titled “Context-aware menus”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 selectionsnull- Hide the menu in that contexttrue- Show all valid items for that context
domternal-floating-menu
Section titled “domternal-floating-menu”A menu that appears on empty lines, allowing users to insert block-level content.
Selector: domternal-floating-menu
Inputs
Section titled “Inputs”| Input | Type | Default | Description |
|---|---|---|---|
editor | Editor | required | The editor instance. |
shouldShow | function | - | Custom predicate to control visibility. Default: shows on empty paragraphs. |
offset | number | 0 | Pixel offset from the cursor position. |
Custom content
Section titled “Custom content”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>domternal-emoji-picker
Section titled “domternal-emoji-picker”A searchable emoji picker panel that opens from the toolbar emoji button.
Selector: domternal-emoji-picker
Inputs
Section titled “Inputs”| Input | Type | Default | Description |
|---|---|---|---|
editor | Editor | required | The editor instance. |
emojis | EmojiPickerItem[] | required | Array 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.
Reactive forms
Section titled “Reactive forms”The editor component implements ControlValueAccessor, so it works with ngModel and reactive forms.
ngModel
Section titled “ngModel”@Component({ imports: [FormsModule, DomternalEditorComponent], template: ` <domternal-editor [extensions]="extensions" [(ngModel)]="content" /> <pre>{{ content }}</pre> `,})export class MyComponent { extensions = [StarterKit]; content = '<p>Initial content</p>';}Reactive forms
Section titled “Reactive forms”@Component({ imports: [ReactiveFormsModule, DomternalEditorComponent], template: ` <domternal-editor [extensions]="extensions" [formControl]="editorControl" /> `,})export class MyComponent { extensions = [StarterKit]; editorControl = new FormControl('<p>Initial content</p>');}Output format
Section titled “Output format”By default, the form value is HTML. Set outputFormat="json" to get JSON instead:
<domternal-editor [extensions]="extensions" [(ngModel)]="content" outputFormat="json"/>Disabled state
Section titled “Disabled state”Reactive forms disable() and enable() automatically toggle the editor’s editable state:
this.editorControl.disable(); // Editor becomes read-onlythis.editorControl.enable(); // Editor becomes editableSignals pattern
Section titled “Signals pattern”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; });}Why signals?
Section titled “Why signals?”- 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()
Dark mode
Section titled “Dark mode”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');}Custom icons
Section titled “Custom icons”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.
NgZone
Section titled “NgZone”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()); }); });}Component reference
Section titled “Component reference”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]Requirements
Section titled “Requirements”- Angular 17.1.0 or later
@domternal/core0.2.0 or later- All components use
ChangeDetectionStrategy.OnPushandViewEncapsulation.None