Getting Started
Overview
Installation
Theme
Forms
Basic Inputs
Custom Inputs
Select
Components
Components
78
Micro Charts
Navigation
Libraries
Content Editor
Data View
Form Builder
Headless Editor
Image Designer
Kanban Board
PDF Builder
PDF Signer
PDF Viewer
Video Player
GitHub repositoryGitHub starsGitHub forksNgStarter UI npm versionNgStarter UI npm downloads per month

Component Blocks

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.

Loading example…

Registering a component

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.
  • If only one of them is defined, it is used in both modes.

Writing the component

@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);
  }
}
  • The block element (tagName) becomes the component host and is not editable by the browser.
  • The block is passed to a block input when the component declares one. Implement NgsHeadlessEditorBlockComponent to get the shape checked.
  • The component runs in the injector of the surface, so it can inject NgsHeadlessEditor and services provided by plugins.
  • Write changes with updateBlock() or removeBlock(); they are recorded in history and can be undone.

Lifecycle

  • The instance is created when the block is first rendered.
  • When the block changes but keeps its id and type, the same instance receives the new block input; its DOM, focus, and internal state are kept.
  • When the block is removed, its type changes, the plugin set changes, or the read-only mode switches components, the instance is destroyed.

Inputs inside blocks

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().

Rich text inside a block

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.

Styling

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.