

A block can be rendered by an Angular component. Use component blocks for embeds, callouts, images with captions, charts, checklists, or anything with its own controls.
const calloutPlugin = defineNgsHeadlessEditorPlugin({
id: 'callout',
blocks: [
{
type: 'callout',
tagName: 'aside',
create: () => createCalloutBlock({ tone: 'info', text: '' }),
isEmpty: () => false,
editorComponent: CalloutBlock,
rendererComponent: CalloutPreview
}
]
});editorComponent renders the block while the surface is editable.rendererComponent renders it while the surface is disabled or the editor is read-only.@Component({
selector: 'app-callout-block',
template: `
<textarea [value]="text()" (input)="setText($event)"></textarea>
<button (click)="remove()">Remove</button>
`
})
export class CalloutBlock implements NgsHeadlessEditorBlockComponent<null> {
private readonly editor = inject(NgsHeadlessEditor);
readonly block = input.required<NgsHeadlessEditorBlock<null>>();
readonly text = computed(() => String(this.block().attrs?.['text'] ?? ''));
setText(event: Event): void {
const text = (event.target as HTMLTextAreaElement).value;
this.editor.updateBlock(this.block().id, { attrs: { ...this.block().attrs, text } });
}
remove(): void {
this.editor.removeBlock(this.block().id);
}
}tagName) becomes the component host and is not editable by the browser.block input when the component declares one. Implement NgsHeadlessEditorBlockComponent to get the shape checked.NgsHeadlessEditor and services provided by plugins.updateBlock() or removeBlock(); they are recorded in history and can be undone.block input; its DOM, focus, and internal state are kept. Events that start inside a component block, such as typing in a textarea, pressing shortcuts in an input, or composing text with an IME, belong to that control. The surface ignores them, so the editor does not intercept typing, Ctrl+Z works natively in the field, and the component decides when to call updateBlock().
For formatted text inside a component, such as a caption, a callout body, or a table cell, use a nested editor instead of a plain field. provideNgsHeadlessEditorInlineRegion() gives the component its own small editor that reuses the marks, commands, and shortcuts of the document editor and records changes in the document history. While active it is the document editor's inline target, so the regular toolbar formats it. The table plugin is built this way.
@Component({
selector: 'app-caption-block',
imports: [NgsHeadlessEditorSurface, NgsHeadlessEditorRuns],
providers: [provideNgsHeadlessEditorInlineRegion()],
templateUrl: './caption-block.html'
})
export class CaptionBlock {
private readonly region = inject(NgsHeadlessEditorInlineRegion);
readonly block = input.required<NgsHeadlessEditorBlock<null>>();
readonly caption = computed(() => normalizeNgsHeadlessEditorTableCell(this.block().attrs?.['caption']));
readonly editing = signal(false);
constructor() {
this.region.configure({ marks: ['bold', 'italic', 'link'] }); // or true / false
effect(() => {
const content = this.region.content(); // nested editor -> block
if (this.editing() && untracked(() => this.region.editor.origin()) !== 'external') {
untracked(() => this.region.parent.updateBlock(this.block().id, {
attrs: { ...this.block().attrs, caption: content }
}));
}
});
}
edit(): void {
this.region.load(this.caption());
this.region.activate(); // toolbar now formats the caption
this.editing.set(true);
}
done(): void {
this.region.deactivate();
this.editing.set(false);
}
}
<!-- caption-block.html -->
@if (editing()) {
<figcaption ngsHeadlessEditorSurface ariaLabel="Caption"></figcaption>
} @else {
<figcaption tabindex="0" [ngsHeadlessEditorRuns]="caption()" (focus)="edit()"></figcaption>
} Content is exchanged as text runs. Render inactive content with ngsHeadlessEditorRuns, which uses the same mark definitions as the surface.
Component styles are encapsulated as usual. The host element is created by the surface, so style it from the surface with ::ng-deep or from the component with :host.