Advanced Usage Guide

July 31, 2026 ยท View on GitHub

This guide covers advanced integration patterns, custom extensions, and the internal architecture of the Angular Tiptap Editor.

๐Ÿงฉ Embedding Angular Components (Angular Nodes)

One of the most powerful features of this library is the ability to turn any Angular component into a TipTap node without writing complex extension code.

1. Define your component

Your component can optionally inherit from AteAngularNodeView to access the full TipTap API (editor, node, attributes, updateAttributes) via Signals.

import { Component, computed } from "@angular/core";
import { AteAngularNodeView } from "@flogeez/angular-tiptap-editor";

@Component({
  selector: "app-my-counter",
  template: `
    <div class="counter-node">
      <button (click)="increment()">Count: {{ count() }}</button>
    </div>
  `,
})
export class MyCounterComponent extends AteAngularNodeView {
  readonly count = computed<number>(() => (this.attributes()["count"] as number) || 0);

  increment() {
    this.updateAttributes({ count: this.count() + 1 });
  }
}

2. Register the Node

Map your component to the editor document structure via the angularNodes config:

editorConfig: AteEditorConfig = {
  angularNodes: [
    {
      component: MyCounterComponent,
      name: "counter",
      attributes: { count: { default: 0 } },
      group: "block",
      draggable: true,
      selectable: true,
    },
  ],
};

โšก Programmatic Control with AteEditorCommandsService

The AteEditorCommandsService provides deep programmatic control over editor instances and exposes the reactive state.

import { AteEditorCommandsService } from "@flogeez/angular-tiptap-editor";

export class MyToolbarComponent {
  private editorCommands = inject(AteEditorCommandsService);

  // Execute commands on a specific editor instance
  clear(editor: Editor) {
    this.editorCommands.clearContent(editor);
  }

  // Access the live reactive state via Signal
  state = this.editorCommands.editorState; // WritableSignal<AteEditorState>
}

๐Ÿ› ๏ธ Custom Tiptap Extensions

You can still use any standard Tiptap extension. Pass them via the tiptapExtensions property.

import Highlight from "@tiptap/extension-highlight";

editorConfig: AteEditorConfig = {
  tiptapExtensions: [Highlight.configure({ multicolor: true })],
};

Extending Reactive State (Calculators)

Any standard Mark or Node you add is automatically tracked by our DiscoveryCalculator. However, for complex calculations, you can define your own Calculator.

import { AteStateCalculator } from "@flogeez/angular-tiptap-editor";

// Called on every editor update
export const MyDepthCalculator: AteStateCalculator = editor => ({
  custom: { selectionDepth: editor.state.selection.$from.depth },
});

// Register it
editorConfig: AteEditorConfig = {
  stateCalculators: [MyDepthCalculator],
};

๐Ÿ–ผ๏ธ Advanced Image Handling

Custom Upload Handler

By default, images are converted to base64. Provide a custom handler to upload to your server (S3, Cloudinary, etc.):

uploadHandler: AteImageUploadHandler = async ctx => {
  const formData = new FormData();
  formData.append("image", ctx.file);

  const response = await fetch("/api/upload", { method: "POST", body: formData });
  const data = await response.json();

  return { src: data.url }; // Return the final URL
};

The ctx object provides file, width, height, type, and base64 (fallback).


โŒจ๏ธ Custom Slash Commands

You can define entirely new commands for the slash menu (/):

slashCommands: AteSlashCommandsConfig = {
  custom: [
    {
      title: "AI Action",
      description: "Generate content with AI",
      icon: "auto_fix",
      keywords: ["ai", "magic", "generate"],
      command: editor => {
        // Your logic here
        editor.commands.insertContent("โœจ Generated content...");
      },
    },
  ],
};

๐Ÿ—๏ธ Architecture Overview

Reactive State Management

The library uses a Snapshot & Signal pattern:

  1. State Snapshot: Every transaction triggers "Calculators" that produce an immutable state object.
  2. Signals Integration: This snapshot is stored in a Signal, ensuring OnPush components only re-render when their specific data changes.

Isolated Instances

Each AngularTiptapEditorComponent provides its own services at the component level. You can have 10 editors on the same page; they will all have independent states, configurations, and upload handlers without any interference.

Core Services

  • AteEditorCommandsService: Centralized API for commands and state.
  • AteEditorRegistry: Global root service tracking all editor instances and the active focused editor.
  • AteImageService: Image processing pipeline (compression, selection).
  • AteI18nService: Reactive translation and locale management.

๐Ÿ“‹ Table of Contents Component (AteTableOfContentsComponent)

The AteTableOfContentsComponent provides a Notion-style, responsive Table of Contents:

import { AteTableOfContentsComponent } from "@flogeez/angular-tiptap-editor";

@Component({
  imports: [AteTableOfContentsComponent],
  template: `
    <!-- Floating Notion-style TOC (auto-connects to active editor) -->
    <ate-table-of-contents [floating]="true" position="right" variant="card" />

    <!-- Or inline TOC targeting a specific editor by ID -->
    <ate-table-of-contents [editor]="'doc-editor'" variant="minimal" />
  `
})

Options & Inputs

InputTypeDefaultDescription
[editor]Editor | AteEditorRef | stringundefinedTarget editor instance, ref, or ID. Fallback: AteEditorRegistry.activeEditor()
[floating]booleanfalseFloating fixed positioning mode
[position]'left' | 'right''right'Floating alignment position
[variant]'card' | 'minimal' | 'transparent''card'Visual container variant
[hoverExpand]booleantrueCollapses into dashes until hovered (floating mode)
[showTitle]booleantrueToggle header title visibility

๐Ÿ—‚๏ธ Global Editor Registry (AteEditorRegistry)

The AteEditorRegistry service is provided in root and tracks all editor instances:

import { inject } from "@angular/core";
import { AteEditorRegistry } from "@flogeez/angular-tiptap-editor";

export class WorkspaceComponent {
  private registry = inject(AteEditorRegistry);

  // Access the currently active (focused) editor facade
  getActiveContent() {
    const activeRef = this.registry.activeEditor();
    if (activeRef) {
      console.log("Active Editor ID:", activeRef.id);
      console.log("Markdown:", activeRef.getContent("markdown"));
      activeRef.commands.toggleBold();
    }
  }

  // Access a specific editor by ID
  targetEditor() {
    const editorRef = this.registry.get("my-editor-id");
    editorRef?.commands.insertTable();
  }
}