CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tiptap--vue-3

Vue 3 components and composables for building rich text editors with tiptap

Pending
Overview
Eval results
Files

vue-renderers.mddocs/

Vue Renderers

Utilities for creating custom Vue-based node and mark views, enabling full Vue component integration within editor content.

Capabilities

VueRenderer Class

Utility class for rendering Vue components inside the editor with full component lifecycle support.

/**
 * Renders Vue components inside the editor
 * Handles component lifecycle and Vue context integration
 */
class VueRenderer {
  constructor(component: Component, options: VueRendererOptions);
  
  /** The editor instance */
  editor: Editor;
  
  /** The Vue component to render */
  component: Component;
  
  /** The DOM element container */
  el: Element | null;
  
  /** Reactive props object */
  props: Record<string, any>;
  
  /** Get the rendered DOM element */
  get element(): Element | null;
  
  /** Get the component reference (exposed API or proxy) */
  get ref(): any;
  
  /**
   * Update component props reactively
   * @param props - New props to merge
   */
  updateProps(props?: Record<string, any>): void;
  
  /**
   * Render the component and return rendered details
   * @private Internal method for component rendering
   */
  renderComponent(): {
    vNode: ReturnType<typeof h> | null;
    destroy: () => void;
    el: Element | null;
  };
  
  /** Destroy the renderer and cleanup */
  destroy(): void;
}

interface VueRendererOptions {
  /** Editor instance */
  editor: Editor;
  /** Initial props for the component */
  props?: Record<string, any>;
}

Key Features:

  • Full Vue component lifecycle support
  • Reactive props updates
  • Vue app context integration
  • Support for both Composition and Options API
  • Automatic cleanup and memory management

Usage Examples:

import { VueRenderer } from "@tiptap/vue-3";
import MyComponent from "./MyComponent.vue";

// Create a renderer
const renderer = new VueRenderer(MyComponent, {
  editor: editorInstance,
  props: {
    title: "Hello World",
    count: 0,
  },
});

// Access the DOM element
const element = renderer.element;

// Access component methods/data
const componentRef = renderer.ref;
componentRef.someMethod();

// Update props
renderer.updateProps({ count: 1 });

// Cleanup
renderer.destroy();

VueNodeViewRenderer Function

Creates node view renderers for Vue components, allowing custom node types to be rendered as Vue components.

/**
 * Creates a node view renderer for Vue components
 * @param component - Vue component to render
 * @param options - Optional configuration
 * @returns NodeViewRenderer function
 */
function VueNodeViewRenderer(
  component: Component,
  options?: VueNodeViewRendererOptions
): NodeViewRenderer;

interface VueNodeViewRendererOptions extends NodeViewRendererOptions {
  /**
   * Custom update function to control when node view updates
   * @param props - Update parameters
   * @returns Whether to update the node view
   */
  update?: ((props: {
    oldNode: ProseMirrorNode;
    oldDecorations: readonly Decoration[];
    oldInnerDecorations: DecorationSource;
    newNode: ProseMirrorNode;
    newDecorations: readonly Decoration[];
    innerDecorations: DecorationSource;
    updateProps: () => void;
  }) => boolean) | null;
}

const nodeViewProps: {
  editor: { type: PropType<Editor>; required: true };
  node: { type: PropType<ProseMirrorNode>; required: true };
  decorations: { type: PropType<DecorationWithType[]>; required: true };
  selected: { type: PropType<boolean>; required: true };
  extension: { type: PropType<Node>; required: true };
  getPos: { type: PropType<() => number | undefined>; required: true };
  updateAttributes: { type: PropType<(attributes: Record<string, any>) => void>; required: true };
  deleteNode: { type: PropType<() => void>; required: true };
  view: { type: PropType<EditorView>; required: true };
  innerDecorations: { type: PropType<DecorationSource>; required: true };
  HTMLAttributes: { type: PropType<Record<string, any>>; required: true };
};

Usage Examples:

// Define a custom node extension
import { Node } from "@tiptap/core";
import { VueNodeViewRenderer } from "@tiptap/vue-3";
import CustomNodeComponent from "./CustomNodeComponent.vue";

const CustomNode = Node.create({
  name: 'customNode',
  
  group: 'block',
  
  content: 'inline*',
  
  parseHTML() {
    return [{ tag: 'div[data-type="custom-node"]' }];
  },
  
  renderHTML({ HTMLAttributes }) {
    return ['div', { 'data-type': 'custom-node', ...HTMLAttributes }, 0];
  },
  
  addNodeView() {
    return VueNodeViewRenderer(CustomNodeComponent);
  },
});

// Use in editor
const editor = useEditor({
  extensions: [StarterKit, CustomNode],
  content: '<p>Hello world</p>',
});

Custom Node Component:

<!-- CustomNodeComponent.vue -->
<template>
  <NodeViewWrapper class="custom-node">
    <div class="node-header">
      <button @click="deleteNode">Delete</button>
      <span>{{ node.attrs.title || 'Untitled' }}</span>
    </div>
    <NodeViewContent class="content" />
  </NodeViewWrapper>
</template>

<script setup>
import { NodeViewContent, NodeViewWrapper, nodeViewProps } from "@tiptap/vue-3";

const props = defineProps(nodeViewProps);

const deleteNode = () => {
  props.deleteNode();
};
</script>

VueMarkViewRenderer Function

Creates mark view renderers for Vue components, allowing custom mark types to be rendered as Vue components.

/**
 * Creates a mark view renderer for Vue components
 * @param component - Vue component to render
 * @param options - Optional configuration
 * @returns MarkViewRenderer function
 */
function VueMarkViewRenderer(
  component: Component,
  options?: VueMarkViewRendererOptions
): MarkViewRenderer;

interface VueMarkViewRendererOptions extends MarkViewRendererOptions {
  /** HTML tag to render as */
  as?: string;
  /** CSS class name */
  className?: string;
  /** HTML attributes */
  attrs?: { [key: string]: string };
}

const markViewProps: {
  editor: { type: PropType<Editor>; required: true };
  mark: { type: PropType<Mark>; required: true };
  extension: { type: PropType<Mark>; required: true };
  inline: { type: PropType<boolean>; required: true };
  view: { type: PropType<EditorView>; required: true };
  updateAttributes: { type: PropType<(attributes: Record<string, any>) => void>; required: true };
  HTMLAttributes: { type: PropType<Record<string, any>>; required: true };
};

/**
 * Component for rendering mark view content
 */
const MarkViewContent: DefineComponent<{
  as?: {
    type: PropType<string>;
    default: 'span';
  };
}>;

Usage Examples:

// Define a custom mark extension
import { Mark } from "@tiptap/core";
import { VueMarkViewRenderer } from "@tiptap/vue-3";
import CustomMarkComponent from "./CustomMarkComponent.vue";

const CustomMark = Mark.create({
  name: 'customMark',
  
  parseHTML() {
    return [{ tag: 'span[data-type="custom-mark"]' }];
  },
  
  renderHTML({ HTMLAttributes }) {
    return ['span', { 'data-type': 'custom-mark', ...HTMLAttributes }, 0];
  },
  
  addMarkView() {
    return VueMarkViewRenderer(CustomMarkComponent);
  },
});

Custom Mark Component:

<!-- CustomMarkComponent.vue -->
<template>
  <span class="custom-mark" :style="markStyle">
    <MarkViewContent />
    <button @click="removeMark" class="remove-btn">×</button>
  </span>
</template>

<script setup>
import { MarkViewContent, markViewProps } from "@tiptap/vue-3";
import { computed } from 'vue';

const props = defineProps(markViewProps);

const markStyle = computed(() => ({
  backgroundColor: props.mark.attrs.color || '#ffeb3b',
  padding: '2px 4px',
  borderRadius: '2px',
}));

const removeMark = () => {
  props.updateAttributes(null); // Remove the mark
};
</script>

Advanced Renderer Patterns

Node View with State Management:

<!-- StatefulNodeComponent.vue -->
<template>
  <NodeViewWrapper>
    <div class="stateful-node">
      <div class="controls">
        <button @click="increment">Count: {{ count }}</button>
        <button @click="updateAttributes({ persistent: count })">Save</button>
      </div>
      <NodeViewContent />
    </div>
  </NodeViewWrapper>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { NodeViewWrapper, NodeViewContent, nodeViewProps } from "@tiptap/vue-3";

const props = defineProps(nodeViewProps);

const count = ref(props.node.attrs.persistent || 0);

const increment = () => {
  count.value++;
};

onMounted(() => {
  // Component lifecycle works normally
  console.log('Node view mounted');
});
</script>

Custom Update Logic:

import { VueNodeViewRenderer } from "@tiptap/vue-3";

const CustomNode = Node.create({
  addNodeView() {
    return VueNodeViewRenderer(CustomNodeComponent, {
      update: ({ oldNode, newNode, updateProps }) => {
        // Only update if specific attributes changed
        if (oldNode.attrs.title !== newNode.attrs.title) {
          updateProps();
          return true;
        }
        return false;
      },
    });
  },
});

Types

interface NodeViewProps {
  editor: Editor;
  node: ProseMirrorNode;
  decorations: DecorationWithType[];
  selected: boolean;
  extension: Node;
  getPos: () => number | undefined;
  updateAttributes: (attributes: Record<string, any>) => void;
  deleteNode: () => void;
  view: EditorView;
  innerDecorations: DecorationSource;
  HTMLAttributes: Record<string, any>;
}

interface MarkViewProps {
  editor: Editor;
  mark: ProseMirrorMark;
  extension: Mark;
  inline: boolean;
  view: EditorView;
  updateAttributes: (attributes: Record<string, any>) => void;
  HTMLAttributes: Record<string, any>;
}

type Component = DefineComponent | ComponentOptions;
type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView;
type MarkViewRenderer = (props: MarkViewRendererProps) => MarkView;

Install with Tessl CLI

npx tessl i tessl/npm-tiptap--vue-3

docs

core-editor-api.md

editor-management.md

index.md

menu-components.md

vue-components.md

vue-renderers.md

tile.json