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

Content Editor

The NgStarter Angular Content Editor library lets you build and render structured long-form content from JSON blocks. Use ngs-content-editor-builder for editing articles, pages, documentation, knowledge base entries, rich notes, editable descriptions, landing content, and CMS-like content. Use ngs-content-editor-renderer to render saved blocks for previews and display-only content.

Content Editor is a block-based builder for structured long-form content. Use ngs-content-editor-builder for articles, pages, documentation, knowledge base entries, rich notes, editable descriptions, landing content, and CMS-like editing workflows.

Document model

For the complete saved structure of all 16 blocks, JSON Schema, third-party exporter examples, and JSON/HTML import methods, see Block format & import. The builder demo includes an Import action for files or pasted content.

Content Editor uses the Headless Editor document: { version: 1, blocks }. Text fields contain text runs with marks, including bold, italic, links and colors. Block settings and properties are stored in attrs.settings and attrs.props. Lists keep their nested items, and tables use attrs.rows for text runs, with column spans, row spans and widths in attrs.cellMetadata. Text edits, formatting and block operations share one undo/redo history. HTML is not stored in text fields.

import { signal } from '@angular/core';
import { ContentEditorDocument } from '@ngstarter-ui/components/content-editor';
import { createNgsHeadlessEditorText } from '@ngstarter-ui/components/headless-editor';

readonly document = signal<ContentEditorDocument>({
  version: 1,
  blocks: [{
    id: 'intro',
    type: 'heading',
    content: [createNgsHeadlessEditorText('Hello', [{ type: 'bold' }])],
    attrs: { settings: { level: 2 }, props: [] }
  }]
});
<ngs-content-editor-builder
  [content]="document()"
  (contentChanged)="document.set($event)" />

<ngs-content-editor-renderer [content]="document()" />

Content and layout blocks

  • callout: text runs with note, tip, warning or danger in attrs.settings.variant.
  • toggle: rich title and nested content.blocks; attrs.settings.expanded controls the initial reader state.
  • attachment: URL, name, size in bytes and MIME type.
  • gallery: image carousel with IDs, alt text and plain text captions. Select a thumbnail to edit that image; drag its handle to reorder. The preview menu can replace, move or remove the selected image.
  • grid: ordered collections in content.cells, each with its own ID and nested blocks. Settings control 2, 3 or 4 columns, cell spacing and stacking on small screens.

Nested blocks use the same formatting and undo/redo history. Drag blocks between collections using their handles. Changing the grid column count reflows the same cells without deleting or merging their content. Cells can be added, reordered and deleted with undo/redo.

Empty nested collections show a paragraph. Typing stays in that paragraph; press Enter to add the next one. Use the plus control beside a block to open the shared block menu.

All Add block controls share the root builder’s blockMenu(). Custom blocks can reuse it with ngsMenuTriggerFor and pass { target: { scope, afterId } } through ngsMenuTriggerData. Resolve the root through inject(CONTENT_BUILDER).rootBuilder. Use an empty scope for the root document, parentId for a toggle, or parentId and cellId for a grid cell. Omit afterId to append to the collection.

Configure options.attachment.uploadFn and options.gallery.uploadFn as (file, dataUrl) => Promise<string> returning a persistent URL from your storage. The default saves a data URL in the document. Upload failures can be retried, and cancelling prevents a pending result from changing the document.

HTML export and configuration

Every built-in block has a standalone HTML converter in CONTENT_EDITOR_DEFAULT_HTML_CONVERTERS, and block definitions expose toHtml. Export the current editor with builder.toHtml(), or a saved document with ContentEditorHtmlSerializer. The renderer also exposes toHtml(), respecting its blocks input.

Register application defaults using provideContentEditorConfig in application or route environment providers. Child environments inherit the parent configuration. Individual block options merge, so changing a converter preserves an existing upload callback.

import { ApplicationConfig } from '@angular/core';
import { provideContentEditorConfig } from '@ngstarter-ui/components/content-editor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideContentEditorConfig({
      blocks: {
        paragraph: {
          toHtml: (block, ctx) =>
            '<article>' + ctx.renderText(block.content) + '</article>'
        },
        image: {
          options: {
            uploadFn: (file: File, dataUrl: string) => uploadImage(file, dataUrl)
          }
        }
      }
    })
  ]
};

// Replace with your application's storage integration.
declare function uploadImage(file: File, dataUrl: string): Promise<string>;

Pass [config] for an editor or renderer instance, or supply a config to toHtml(config) for one export. Priority is built-in defaults, environment configuration, instance configuration, then the export call. Existing [options] overrides shared block options.

import { Injectable, inject } from '@angular/core';
import {
  ContentEditorConfig, ContentEditorDocument, ContentEditorHtmlSerializer,
  contentEditorToHtml, contentEditorBlockToHtml
} from '@ngstarter-ui/components/content-editor';

@Injectable({ providedIn: 'root' })
export class ArticleHtmlExport {
  private readonly serializer = inject(ContentEditorHtmlSerializer);
  toHtml(document: ContentEditorDocument): string {
    return this.serializer.toHtml(document); // Includes environment defaults.
  }
}

export const editorConfig: ContentEditorConfig = {
  blocks: {
    toggle: {
      toHtml: (block, ctx) =>
        '<section>' + ctx.defaultToHtml(block) + '</section>'
    },
    custom: {
      toHtml: (block, ctx) =>
        '<div>' + ctx.escape(block.content.title) + '</div>'
    }
  }
};

// Pure functions outside Angular DI, including server-side usage:
export function exportSavedDocument(document: ContentEditorDocument) {
  return {
    html: contentEditorToHtml(document, editorConfig),
    firstBlockHtml: document.blocks[0]
      ? contentEditorBlockToHtml(document.blocks[0], editorConfig) : ''
  };
}
<ngs-content-editor-builder
  #builder="ngsContentEditorBuilder"
  [content]="document()"
  [config]="editorConfig"
  (contentChanged)="document.set($event)" />

<button ngsButton (click)="saveHtml(builder.toHtml())">Save HTML</button>

Converters receive the saved block and a context with renderText, renderBlock, renderBlocks, escape and safeUrl. Nested blocks use the same configured converters. Use defaultToHtml to extend the built-in converter without recursion. Custom text marks can use config.marks.

HTML export runs without editor DOM and preserves rich text, lists, table metadata and media. Toggle blocks become native details elements, galleries use native horizontal scrolling, and responsive grid styles are included once per export. Set includeStyles: false if your page supplies the exported layout styles itself. Converters may add shared CSS with ctx.addStyles.

The pure contentEditorToHtml(document, config) and contentEditorBlockToHtml(block, config) functions use only the supplied configuration; the injected serializer also reads environment defaults. Register a converter for custom block types. Unknown blocks throw unless unknownBlockToHtml is explicitly configured. Custom converters produce application-owned markup; use the context helpers to escape saved text and attributes.

Use it for:
  • Building content from blocks such as paragraph, heading, list, quote, code, divider, table, image, video, embed, callout, toggle, attachment, gallery, and grid.
  • Reordering, duplicating, deleting, and multi-selecting blocks inside an editor surface.
  • Persisting structured ContentEditorDocument data through the contentChanged event.
  • Passing block options such as image, video, attachment and gallery upload functions.
  • Rendering saved blocks with ngs-content-editor-renderer in preview dialogs, read-only pages, and CMS display surfaces.

Do not use Content Editor for quick messages, comments, threads, or reply composers; use Comment Editor for that. Do not use it for a simple text field or a small rich text input; use Text Editor, Input, or Textarea depending on the task. For display-only content, render the saved block JSON with ngs-content-editor-renderer.