Rich Text Editor
A rich-text editor whose value is sanitized HTML. The editing surface is a contenteditable element, but the browser never edits it on its own: every keystroke, toolbar click and paste becomes a command on the editor's own document model, and the DOM is rendered from that model. There is no document.execCommand — deprecated, different in every engine, and the reason most editors produce different markup per browser — so the output is the same everywhere and undo/redo is the editor's own history.
The same allowlist parser reads the bound value, every paste (Word and Google Docs markup is cleaned up on the way in) and insertHtml(). Scripts, event handlers, unknown attributes and unsafe URLs cannot survive it; its one DOMParser call runs behind the oge-ui#editor Trusted Types policy, and the live DOM is built with createElement, never innerHTML. Full API reference →
Getting started
Type, select and format. The box under the editor is the live value — sanitized HTML, the empty string when the document is empty.
Hello world
<p>Hello <strong>world</strong></p>
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeEditor } from '@oge-ui/editor';
@Component({
selector: 'demo-root',
imports: [OgeEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The value is sanitized HTML ('' when empty). Every edit is a command on
the editor's own document model — no document.execCommand — so every
browser produces the same markup, and undo is the editor's own history. -->
<oge-editor
label="Description"
placeholder="Write something…"
hint="Try Ctrl+B, Ctrl+K, or type “# ” at the start of a line."
[(value)]="html"
/>
<pre>{{ html() }}</pre>
`,
})
export class Demo {
protected readonly html = signal('<p>Hello <strong>world</strong></p>');
}Toolbar and custom tools
Built-in tool names, separators and your own tools in any order. Narrow the window: tools that do not fit move into the overflow menu, toggles keep their check mark there.
1.2.0
- New rich-text editor
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeEditor } from '@oge-ui/editor';
import type { OgeEditorCustomTool, OgeEditorToolbarEntry } from '@oge-ui/editor';
@Component({
selector: 'demo-root',
imports: [OgeEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Built-in tool names, 'separator' and your own tools, in any order.
The toolbar is the APG toolbar from @oge-ui/layout: one Tab stop, arrow
keys between tools, and what does not fit moves into an overflow menu. -->
<oge-editor
label="Release notes"
[toolbar]="tools"
[(value)]="notes"
/>
`,
})
export class Demo {
/** A custom tool: a label, an optional 16×16 stroke icon, and what it does. */
private readonly stamp: OgeEditorCustomTool = {
key: 'stamp',
text: 'Insert today’s date',
icon: 'M3 4.5h10v9H3zM3 7.5h10M6 2.5v3M10 2.5v3',
run: (editor) => editor.insertText(new Date().toLocaleDateString('en-GB')),
};
protected readonly tools: readonly OgeEditorToolbarEntry[] = [
'undo',
'redo',
'separator',
'blockFormat',
'bold',
'italic',
'separator',
'bulletList',
'orderedList',
'separator',
'link',
this.stamp,
];
protected readonly notes = signal('<h3>1.2.0</h3><ul><li>New rich-text editor</li></ul>');
}Keyboard and markdown
Every shortcut is in its tool’s tooltip and aria-keyshortcuts. Start a line with # , - , 1. or > ; Tab and Shift+Tab nest list items.
| Ctrl/⌘ + B, I, U | Bold, italic, underline |
|---|---|
| Ctrl/⌘ + Shift + X | Strikethrough |
| Ctrl/⌘ + K | Link dialog |
| Ctrl/⌘ + Z, Y | Undo, redo |
| Ctrl/⌘ + Alt + 0 … 6 | Paragraph, heading 1–6 |
| Ctrl/⌘ + Shift + 7, 8 | Numbered, bulleted list |
| Shift + Enter | Line break inside the block |
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeEditor } from '@oge-ui/editor';
import type { OgeEditorHeadingLevel } from '@oge-ui/editor';
@Component({
selector: 'demo-root',
imports: [OgeEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Markdown shortcuts: "# " … "###### ", "- ", "1. ", "> ", "\`\`\` " at the
start of a paragraph, and "---" + Enter for a horizontal line. One undo
brings the typed characters back. Tab / Shift+Tab nest list items; Tab
anywhere else leaves the editor, so the keyboard is never trapped. -->
<oge-editor
label="Meeting notes"
[markdownShortcuts]="true"
[headingLevels]="levels"
[(value)]="notes"
/>
`,
})
export class Demo {
protected readonly levels: readonly OgeEditorHeadingLevel[] = [1, 2, 3];
protected readonly notes = signal('');
}Links and images
The link and image tools open the overlay’s prompt dialog. A javascript: address is refused, and a bare domain typed into the dialog gets https:// instead of becoming a relative link.
Read the docs.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeEditor } from '@oge-ui/editor';
@Component({
selector: 'demo-root',
imports: [OgeEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The link and image tools open the overlay's prompt dialog; the methods
below do the same from code. Every address goes through the URL
allowlist — a javascript: link is refused, and "ogeui.com" typed into
the dialog becomes https://ogeui.com instead of a relative link. -->
<oge-editor #editor label="Article" [allowedSchemes]="['web+app']" [(value)]="article" />
<button type="button" (click)="editor.insertLink('https://www.ogeui.com', 'OGE UI')">
Insert a link
</button>
<button type="button" (click)="editor.insertImage('/favicon.ico', 'OGE logo')">
Insert an image
</button>
<button type="button" (click)="editor.openLinkDialog()">Open the link dialog</button>
`,
})
export class Demo {
protected readonly article = signal('<p>Read the docs.</p>');
}Paste and sanitizing
Paste from Word, Google Docs or a web page: formatting the editor knows survives, everything else is dropped. The second box shows the sanitizer applied to hostile HTML.
<p>Hi there<img src="x" alt=""></p>
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeEditor, ogeSanitizeEditorHtml } from '@oge-ui/editor';
import type { OgeEditorPastingEvent } from '@oge-ui/editor';
@Component({
selector: 'demo-root',
imports: [OgeEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- One allowlist parser handles the bound value, pastes (Word and Google
Docs markup is cleaned up) and insertHtml(). (pasting) is cancelable and
may rewrite the payload; pasteMode="text" keeps text only. -->
<oge-editor
label="Paste here"
pasteMode="html"
(pasting)="onPasting($event)"
[(value)]="pasted"
/>
<p>Sanitized on the server side too:</p>
<pre>{{ clean }}</pre>
`,
})
export class Demo {
protected readonly pasted = signal('');
/** The same sanitizer, for HTML that did not come through the editor. */
protected readonly clean = ogeSanitizeEditorHtml(
'<p>Hi <a href="javascript:alert(1)">there</a><img src=x onerror=alert(1)></p><script>alert(1)</script>',
);
protected onPasting(event: OgeEditorPastingEvent): void {
// refuse pastes that carry tracking pixels, keep everything else
event.cancel = event.html.includes('1x1.gif');
}
}Angular forms
Reactive forms, Signal Forms and plain [(value)] bind the same component. ogeEditorMaxLength() counts text, not markup.
valid: false
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeEditor, ogeEditorMaxLength } from '@oge-ui/editor';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'demo-root',
imports: [OgeEditor, ReactiveFormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- maxLength stops typing and pasting at 200 characters of text and the
counter shows the budget. ogeEditorMaxLength() is the matching validator:
Validators.maxLength would count the HTML markup. Signal Forms binds the
same component through [formField], and [(value)] needs no forms at all. -->
<oge-editor
label="Summary"
[formControl]="summary"
[required]="true"
[maxLength]="200"
counter="characters"
/>
<p>valid: {{ summary.valid }}</p>
`,
})
export class Demo {
protected readonly summary = new FormControl('', {
nonNullable: true,
validators: [Validators.required, ogeEditorMaxLength(200)],
});
}Read-only, sizing and counter
Read-only keeps the text focusable and selectable; disabled takes the editor out of the Tab order. Drag the corner to resize.
Terms
By using this service you agree to everything.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeEditor } from '@oge-ui/editor';
@Component({
selector: 'demo-root',
imports: [OgeEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- readonly keeps the text focusable and selectable; disabled takes the
editor out of the Tab order. height / minHeight / maxHeight take px or any
CSS length; resizable adds a vertical resize handle. -->
<label><input type="checkbox" [checked]="readonly()" (change)="readonly.set(!readonly())" /> Read-only</label>
<label><input type="checkbox" [checked]="disabled()" (change)="disabled.set(!disabled())" /> Disabled</label>
<oge-editor
label="Terms"
[readonly]="readonly()"
[disabled]="disabled()"
[minHeight]="120"
[maxHeight]="260"
[resizable]="true"
counter="both"
[(value)]="terms"
/>
`,
})
export class Demo {
protected readonly readonly = signal(false);
protected readonly disabled = signal(false);
protected readonly terms = signal(
'<h3>Terms</h3><p>By using this service you agree to <em>everything</em>.</p>',
);
}Configuration
App-wide defaults and strings through provideOgeEditorConfig() (also covered by provideOgeLocale()), and the Trusted Types policy name a strict CSP lists.
// app.config.ts — app-wide defaults; a function makes them live
import { provideOgeEditorConfig } from '@oge-ui/editor';
export const appConfig = {
providers: [
provideOgeEditorConfig({
headingLevels: [2, 3],
pasteMode: 'html',
markdownShortcuts: true,
textColors: 'office',
messages: { tools: { bold: 'Kalın', italic: 'İtalik' } },
}),
],
};
// A page with require-trusted-types-for 'script' lists the editor's policy:
// Content-Security-Policy: require-trusted-types-for 'script'; trusted-types angular oge-ui#editor