import { Component } from '@angular/core';
import { WrEditor } from 'ngwr/editor';
@Component({ imports: [WrEditor] })
export class MyComponent {}Editor
Rich-text editor whose value is the document as a JSON tree.
Installation
Import the class, and @use the entry point in your global styles. Nothing warns you when the @use is missing.
Peer dependencies
Install ProseMirror to use the editor.
pnpm add prosemirror-model prosemirror-state prosemirror-view prosemirror-commands prosemirror-keymap prosemirror-history prosemirror-schema-list prosemirror-inputrulesBasic usage
[(value)] is the document as a WrEditorJson tree, or null while nothing is written.
<wr-editor ariaLabel="Agenda" placeholder="What is on the agenda?" [(value)]="agenda" />
<pre>{{ agenda() | json }}</pre>// "Hello, **world**" — one paragraph, two text nodes, one mark.
const value: WrEditorValue = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Hello, ' },
{ type: 'text', text: 'world', marks: [{ type: 'strong' }] },
],
},
],
};Storing HTML or markdown
Convert the JSON yourself: to HTML with prosemirror-model and your own Schema, to markdown with any serializer.
import { DOMSerializer, Node, type Schema } from 'prosemirror-model';
import type { WrEditorJson } from 'ngwr/editor';
// `schema` is your own, declaring the nodes and marks WrEditorJson lists.
export function toHtml(value: WrEditorJson, schema: Schema, document: Document): string {
const doc = Node.fromJSON(schema, value);
const host = document.createElement('div');
host.append(DOMSerializer.fromSchema(schema).serializeFragment(doc.content, { document }));
return host.innerHTML;
}Toolbar
toolbar lists the tools in order, with '|' between groups; false hides it and keeps the shortcuts.
<wr-editor ariaLabel="Comment" placeholder="Leave a comment" [toolbar]="tools" [(value)]="comment" />import { WR_EDITOR_TOOLBAR, type WrEditorTool } from 'ngwr/editor';
// The default toolbar without the two code tools.
readonly tools: readonly WrEditorTool[] = WR_EDITOR_TOOLBAR.filter(
tool => tool !== 'code' && tool !== 'codeBlock'
);<!-- No toolbar; the shortcuts still work. -->
<wr-editor ariaLabel="Notes" [toolbar]="false" [(value)]="notes" />Signal Forms
[formField] binds the value; required() treats an empty editor as empty.
<wr-form-field label="Description" hint="Required.">
<wr-editor placeholder="Describe the change" [formField]="ticket.description" />
</wr-form-field>
<pre>{{ ticket.description().value() | json }}</pre>Read-only and disabled
readonly shows the document without starting ProseMirror; disabled keeps the editor and takes it out of the tab order.
<!-- A viewer: no toolbar, no ProseMirror. -->
<wr-editor ariaLabel="Published notes" readonly [toolbar]="false" [value]="notes" />
<wr-editor ariaLabel="Locked notes" disabled [value]="notes" />Bound to a signal, readonly mounts the editor the first time it turns false.
Keyboard
The toolbar is one tab stop; the arrow keys move between its tools.
| Key | Does |
|---|---|
| Bold, italic, underline. | |
| Strikethrough, inline code. | |
| Paragraph, heading 1 to 3. | |
| Bulleted list, numbered list. | |
| Quote, code block. | |
| Indent, outdent a list item. | |
| Open the link panel. | |
Undo, redo. Ctrl + Y is not bound on Apple keyboards. | |
| Line break. | |
| Leave a code block or a table onto a new paragraph. | |
#, ##, ###, >, -, 1. then ``` | At the start of a line: heading, quote, list, code block. |
Untrusted input
A bound value with an unknown node or mark, or a link or image the URL check refuses, is refused whole.
// Refused whole: the URL check never lets a javascript: link through.
protected readonly imported = signal<WrEditorValue>({
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'a script link',
marks: [{ type: 'link', attrs: { href: 'javascript:alert(1)', title: null } }],
},
],
},
],
});- A refused value leaves the current document on screen, makes the editor read-only (
wr-editor--refused) and warns in dev mode. - Pasted content is cleaned against the same schema.
Gotchas
| What happens | What to do |
|---|---|
| With no name, every editor is announced as "Rich text". | Set ariaLabel, or put it in a <wr-form-field label>. |
Tab leaves the editor; it does not indent. | Indent list items with Mod + ] and Mod + [. |
On Windows layouts with AltGr, the Mod + Alt chords type characters instead. | Set block types from the toolbar. |
A CSP trusted-types allowlist breaks paste. | Add ProseMirrorClipboard to the allowlist. |
Bundle cost
Wrap the editor in @defer to keep ProseMirror off the first paint.
@defer (on viewport) {
<wr-editor [formField]="form.body" />
} @placeholder {
<div class="editor-placeholder" aria-hidden="true"></div>
}What it does not do
- Insert tables or images. Bound ones are kept, and table text stays editable.
- Toggle a task item's box.
- Colours, fonts, sizes, alignment, or headings past level three.
- Mentions, collaborative editing, custom nodes or custom tools.
API
| Name | Description | Type | Default |
|---|---|---|---|
value | The document as a {@link WrEditorJson} tree; null when nothing is written. Bound by [formField], or two-way via [(value)]. | WrEditorValue | null |
(touch) | Emitted on blur so a bound field can mark itself touched. | void | — |
disabled | Disable the editor and its toolbar. Bound automatically from the field's disabled state when used with [formField]. | boolean | false |
readonly | Refuse edits while the text stays reachable, selectable and announced. Bound automatically from the field's readonly state when used with [formField]. Set before the editor mounts, it means ProseMirror is never started: the document is drawn as ordinary markup in a focusable role="group" that keeps the tab stop and carries the editor's name, so a viewer costs no editor and the structure of what it shows is still announced. aria-readonly is deliberately absent there — ARIA does not allow it on group, and aria-disabled, the only attribute that would apply, means something else — so the cue is the wr-editor--readonly class. The toolbar goes inert, since none of its tools can apply; a pure viewer binds [toolbar]="false". A view that is already mounted stays mounted, refuses typing, paste, drop, the shortcuts and every toolbar command, and reports aria-readonly on its surface as before. | boolean | false |
placeholder | Hint shown while the document is empty, and exposed as aria-placeholder. | string | '' |
ariaLabel | Accessible name of the text — of the surface, or of the document a read-only editor draws in its place. Falls back to the surrounding <wr-form-field>'s label, then to editor.label. | string | null | null |
toolbar | The toolbar's tools, in order, with '|' between groups; false hides it. | readonly WrEditorTool[] | false | WR_EDITOR_TOOLBAR |
Types
The value shape and the toolbar vocabulary.
| Name | Description | Type | Default |
|---|---|---|---|
WrEditorValue | The value model: a WrEditorJson tree, or null when empty. | WrEditorJson | null | — |
WrEditorJson | The document as plain data, in the shape ProseMirror writes. | interface | — |
typerequired | doc, paragraph, heading, blockquote, code_block, horizontal_rule, bullet_list, ordered_list, list_item, hard_break, image, table, table_row, table_header, table_cell or text. | string | — |
attrs | heading.level 1–3, code_block.language, ordered_list.order, the lists’ tight, list_item.checked (true, false or null), image.src / alt / title, a cell’s align. | Readonly<Record<string, unknown>> | — |
content | The child nodes. | readonly WrEditorJson[] | — |
marks | The marks on a text node. | readonly WrEditorMarkJson[] | — |
text | A text node’s text. | string | — |
WrEditorMarkJson | One mark: strong, em, underline, strike, code, or link with { href, title }. | interface | — |
typerequired | The mark’s name. | string | — |
attrs | A link’s href and title. | Readonly<Record<string, unknown>> | — |
WrEditorTool | One entry of the toolbar input. '|' draws a separator between two groups. | 'bold' | 'italic' | 'underline' | 'strike' | 'code' | 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'bulletList' | 'orderedList' | 'blockquote' | 'codeBlock' | 'link' | 'horizontalRule' | 'undo' | 'redo' | '|' | — |
WR_EDITOR_TOOLBAR | The toolbar drawn when toolbar is not bound — every tool, in five groups. Filter it to build your own. | readonly WrEditorTool[] | — |
CSS variables
Set these on the element itself, with a rule more specific than its Declared on selector. In a global stylesheet, put a class of yours on an ancestor it really has: <body> for an overlay panel, which renders outside your markup. In a component stylesheet the selector alone works, but only for an element in that component's own template. A value on a wrapper or :root is shadowed, because ngwr/editor declares each default on its own element.
| Variable | Default | Declared on |
|---|---|---|
--wr-editor-bg | var(--wr-color-surface) | .wr-editor +1 variant override |
--wr-editor-border | var(--wr-border-base) | .wr-editor +1 variant override |
--wr-editor-color | var(--wr-color-on-surface) | .wr-editor +1 variant override |
--wr-editor-content-cell-padding-x | var(--wr-space-md) | .wr-editor |
--wr-editor-content-cell-padding-y | var(--wr-space-sm) | .wr-editor |
--wr-editor-content-code-font-family | var(--wr-font-family-mono, ui-monospace, monospace) | .wr-editor |
--wr-editor-content-code-radius | var(--wr-border-radius-sm) | .wr-editor |
--wr-editor-content-h1-font-size | var(--wr-text-2xl) | .wr-editor |
--wr-editor-content-h2-font-size | var(--wr-text-xl) | .wr-editor |
--wr-editor-content-h3-font-size | var(--wr-text-lg) | .wr-editor |
--wr-editor-content-heading-font-weight | var(--wr-font-weight-semibold) | .wr-editor |
--wr-editor-content-heading-line-height | var(--wr-leading-tight) | .wr-editor |
--wr-editor-content-image-radius | var(--wr-border-radius-sm) | .wr-editor |
--wr-editor-content-pre-padding | var(--wr-space-md) | .wr-editor |
--wr-editor-content-pre-radius | var(--wr-border-radius-base) | .wr-editor |
--wr-editor-content-strong-font-weight | var(--wr-font-weight-semibold) | .wr-editor |
--wr-editor-content-table-font-size | var(--wr-text-sm) | .wr-editor |
--wr-editor-content-th-font-weight | var(--wr-font-weight-semibold) | .wr-editor |
--wr-editor-font-family | var(--wr-font-family-base) | .wr-editor |
--wr-editor-font-size | var(--wr-text-sm) | .wr-editor |
--wr-editor-line-height | var(--wr-leading-relaxed) | .wr-editor |
--wr-editor-link-actions-gap | var(--wr-space-sm) | .wr-editor-link |
--wr-editor-link-body-gap | var(--wr-space-sm) | .wr-editor-link |
--wr-editor-link-body-padding | var(--wr-space-md) | .wr-editor-link |
--wr-editor-link-error-font-size | var(--wr-text-xs) | .wr-editor-link |
--wr-editor-link-label-font-size | var(--wr-text-sm) | .wr-editor-link |
--wr-editor-link-label-font-weight | var(--wr-font-weight-medium) | .wr-editor-link |
--wr-editor-max-height | none | .wr-editor |
--wr-editor-min-height | 8rem | .wr-editor |
--wr-editor-padding-x | var(--wr-control-padding-x-md) | .wr-editor |
--wr-editor-padding-y | var(--wr-space-sm) | .wr-editor |
--wr-editor-placeholder | var(--wr-color-placeholder) | .wr-editor |
--wr-editor-radius | var(--wr-control-radius-md) | .wr-editor |
--wr-editor-ring | transparent | .wr-editor +1 variant override |
--wr-editor-separator-margin | var(--wr-space-xs) | .wr-editor |
--wr-editor-task-radius | var(--wr-border-radius-sm) | .wr-editor |
--wr-editor-tool-active-bg | var(--wr-color-primary-soft) | .wr-editor |
--wr-editor-tool-active-color | var(--wr-color-primary-ink) | .wr-editor |
--wr-editor-tool-mark | 2px only under .wr-editor__tool--active.wr-btn — unset elsewhere | .wr-editor__tool--active.wr-btn |
--wr-editor-toolbar-bg | var(--wr-color-fill-subtle) | .wr-editor |
--wr-editor-toolbar-border | var(--wr-border-base) | .wr-editor |
--wr-editor-toolbar-gap | var(--wr-space-xs) | .wr-editor |
--wr-editor-toolbar-padding | var(--wr-space-xs) | .wr-editor |
Dependencies
See also
Component TextareaA multi-line text field that can grow with its content.Component MentionSuggest names or tags while typing @ in a text field.Guide FormsBind ngwr controls to any Angular form and show their errors.Component Form FieldWraps a form control with a label, a hint and its error messages.Component ButtonA button for an action or a link.Component FormA plain layout wrapper for a label, a control and its error.Component InputA directive that styles a native <input> or <textarea>.Component PopoverShow a template, or a text tooltip, next to its trigger.