Vue 3 components and composables for building rich text editors with tiptap
—
Utilities for creating custom Vue-based node and mark views, enabling full Vue component integration within editor content.
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:
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();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>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>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;
},
});
},
});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