Search code examples
javascriptvue.jsvuejs3tiptap

How to pass data to custom Vue Extension in TipTap?


I'm trying to pass data to a custom vue component that gets rendered inside the tiptap editor. I can pass default properties but assigning reactive values to it doesn't seem to work.

This is the tiptap-node-extension.js file:

import {Node, mergeAttributes} from '@tiptap/core'
import {VueNodeViewRenderer} from '@tiptap/vue-3'
import Component from '@/views/components/vue-component.vue'

export default Node.create({
    parseHTML() {
        return [{ tag: 'vue-component' }]
    },
    renderHTML({ HTMLAttributes }) {
        return ['vue-component', mergeAttributes(HTMLAttributes)]
    },
    addNodeView() {
        return VueNodeViewRenderer(Component)
    },
})

the script setup portion of the editor component:

<script setup>
import {useEditor, EditorContent, BubbleMenu} from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import {Underline} from "@tiptap/extension-underline";
import {TextAlign} from "@tiptap/extension-text-align";
import {Link} from "@tiptap/extension-link";
import VueComponent from '@/js/tiptap-node-extension.js'


const editor = useEditor({
    extensions: [
        StarterKit,
        TextAlign.configure({ types: ['heading', 'paragraph'] }),
        Underline,
        Link,
        VueComponent.extend({
                name: 'vueComponent',
                group: 'block',
                draggable: true,
                addAttributes() {
                    return {
                        src: {
                            default: '123',
                        }
                    }
                },
            }
        ),
    ],
    content: props.modelValue,
    onUpdate: ({ editor }) => {
        emit('update:modelValue', editor.getHTML())
    },
    editable: props.locked ? false : store.admin
})

const sendDataToExtension = async (editor, event) => {
    // Triggered upon event

    ...

    state.src = '123'
    editor.chain().focus().insertContent('<vue-component/>').run()
}
</script>

and the vue component:

<script setup>
import {NodeViewWrapper} from '@tiptap/vue-3'

const props = defineProps({
    node: {
        type: Object,
        required: true
    },
    updateAttributes: {
        type: Function,
        required: true,
    }
})

</script>


<template>
  <node-view-wrapper class="vue-component" data-drag-handle="">
    <p>{{ node.attrs.src }}</p>
  </node-view-wrapper>
</template>

The default of src gets through but when I try to assign a reactive object (that gets created after mounting the editor component) it ends up being undefined.

This works:

src: {
    default: '123'
}

but this doesn't:

...

src: {
    default: state.src
}

...

const sendDataToExtension = async (editor, event) => {
    // triggered upon event

    ...

    state.src = '123'
    editor.chain().focus().insertContent('<vue-component/>').run()

}

How do I send data to the vue component that is created after mounting editor?

Attempt:

editor.chain().focus().insertContent('<vue-component/>', {src: state.src}).run()

Solution

  • First I would say that I would recommend creating a purpose built extension, instead of having the general VueComponent that you have now. If you extend more based on that extension you will have several extension competing for the tag. Move all code that you set in extend to the actual extentions, you can set any tag-name you want.

    Now to what I believe is the problem here: insertContent look like this:

    insertContent: (value: Content, options?: {
        parseOptions?: ParseOptions;
        updateSelection?: boolean;
    })
    

    Content is declared as

    export declare type Content = HTMLContent | JSONContent | JSONContent[] | null;
    export declare type HTMLContent = string;
    export declare type JSONContent = {
        type?: string;
        attrs?: Record<string, any>;
        content?: JSONContent[];
        marks?: {
            type: string;
            attrs?: Record<string, any>;
            [key: string]: any;
        }[];
        text?: string;
        [key: string]: any;
    };
    

    In your case you will have to add the src attribute to your html string, however I would recommend using the JSONContent type in your case then:

    editor.chain().focus().insertContent({type: "vueComponent", attrs:{src: state.src}}).run()
    

    Here the type is the name that you set of the component.

    Hope this makes sense, the documentation on tiptap is kind of good as well https://tiptap.dev/guide/custom-extensions/#attributes Let me know if you have further issues.