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

Plugins

Everything an editor can do beyond plain paragraphs comes from plugins. A plugin is a typed object; the built-in text and color features are plugins too. This example adds links with paste handling, a shortcut that targets another plugin, a plugin-scoped statistics service, and a draft storage plugin that is installed and removed at runtime.

Loading example…

Anatomy

MemberDescription
idUnique plugin id.
blocksBlock definitions. See Blocks.
marksMark definitions. See Marks.
commandsCommands registered by id. See Commands.
keymapKey bindings to command objects or ids, with optional payloads.
providersAngular providers added to the editor scope.
handlePaste(event, editor)Handles a paste and returns true, or returns false to pass it on.
setup(editor)Runs when the plugin is installed. May return a cleanup function.
export function linkEditorPlugin(): NgsHeadlessEditorPlugin {
  return defineNgsHeadlessEditorPlugin({
    id: 'link',
    marks: [
      {
        type: 'link',
        tagName: 'a',
        parseTags: ['a'],
        applyAttributes: (element, mark) => {
          const href = toSafeHref(String(mark.attrs?.['href'] ?? ''));
          if (href) {
            element.setAttribute('href', href);
            element.setAttribute('rel', 'noopener noreferrer');
          }
        },
        readAttributes: element => {
          const href = toSafeHref(element.getAttribute('href'));
          return href ? { href } : undefined;
        }
      }
    ],
    commands: [SET_LINK, UNSET_LINK],
    keymap: [
      { key: 'Mod-Shift-h', command: NGS_HEADLESS_EDITOR_SET_BACKGROUND_COLOR, payload: '#fef08a' }
    ],
    providers: [EditorStats],
    handlePaste: (event, editor) => { /* see below */ return false; }
  });
}

Installing plugins

@Component({
  providers: [
    provideNgsHeadlessEditor(
      withHeadlessEditorPlugin(basicTextEditorPlugin()),
      withHeadlessEditorPlugin(colorEditorPlugin()),
      withHeadlessEditorPlugin(linkEditorPlugin())
    )
  ]
})
export class NotesEditor {
  // Provided by linkEditorPlugin()
  readonly stats = inject(EditorStats);
}

@Injectable()
export class EditorStats {
  private readonly editor = inject(NgsHeadlessEditor);
  readonly words = computed(() =>
    getNgsHeadlessEditorDocumentText(this.editor.document()).split(/\s+/).filter(Boolean).length
  );
}

withHeadlessEditorPlugin() is the only way to contribute providers: they are added next to the editor, so components and services in the editor scope can inject them.

Plugin ids, command ids, mark types, and block types must be unique across the installed set. A conflict throws an error that names the duplicate.

Changing plugins at runtime

setPlugins() replaces the whole set atomically. Cleanups of the previous set run, then setup() runs for every plugin of the new set, and the surface re-renders because block and mark definitions may have changed. Passing the same plugins in the same order does nothing.

const draftPlugin = draftStoragePlugin('notes-draft');

// Keep the current plugins and add one: its setup() runs.
editor.setPlugins([...editor.plugins(), draftPlugin]);

// Remove it again: its cleanup runs.
editor.setPlugins(editor.plugins().filter(plugin => plugin !== draftPlugin));

Paste handlers

Handlers run in plugin order before the plain-text fallback. Read the clipboard, change the document through the editor, and return true to stop further handling.

handlePaste: (event, editor) => {
  const href = toSafeHref(event.clipboardData?.getData('text/plain'));
  if (!href) {
    return false;                       // not a URL: next handler or plain text
  }
  const selection = editor.selection();
  const collapsed = !selection ||
    (selection.anchor.blockId === selection.focus.blockId &&
     selection.anchor.offset === selection.focus.offset);

  if (!collapsed) {
    return editor.setMark('link', { href });  // link the selected text
  }
  editor.setMark('link', { href });     // arm the mark at the caret
  editor.insertText(href, 'paste');     // insert the URL as linked text
  editor.unsetMark('link');             // continue typing without the link
  return true;
}

Lifecycle

export function draftStoragePlugin(key: string): NgsHeadlessEditorPlugin {
  return defineNgsHeadlessEditorPlugin({
    id: 'draft-storage',
    setup: editor => {
      if (typeof localStorage === 'undefined' || typeof document === 'undefined') {
        return;                               // server: nothing to do
      }
      const saved = localStorage.getItem(key);
      if (saved) {
        editor.setDocument(JSON.parse(saved));  // restore on install
      }
      const persist = () => localStorage.setItem(key, JSON.stringify(editor.document()));
      document.defaultView?.addEventListener('pagehide', persist);

      return () => {                          // on setPlugins() and on destroy
        persist();
        document.defaultView?.removeEventListener('pagehide', persist);
      };
    }
  });
}

setup() also runs during server-side rendering when the editor is created there, so guard browser APIs. Prefer event listeners over timers: a repeating setInterval started inside Angular's zone keeps the application from ever becoming stable, which blocks SSR and tests. If a plugin needs a timer, run it outside the zone.

Configurable plugins

Export a factory function instead of a constant when a plugin has options, and keep command objects as exported constants so toolbars can import them.

For a plugin with custom Angular menu content, see Mentions. Its optionComponent setting supplies a shared option renderer for the editor scope.