Skip to content

Vue Integration

ProseKit is designed to work seamlessly with Vue.

vue
<script setup lang="ts">
import 'prosekit/basic/style.css'

import { createEditor, jsonFromNode, type NodeJSON } from 'prosekit/core'
import { ProseKit, useDocChange } from 'prosekit/vue'
import { ref, watchPostEffect } from 'vue'

import { defineExtension } from './extension'

const props = defineProps<{
  defaultContent?: NodeJSON
}>()

const emit = defineEmits<{
  docUpdate: [doc: NodeJSON]
}>()

const extension = defineExtension()
const editor = createEditor({ extension, defaultContent: props.defaultContent })

useDocChange(
  (doc) => {
    emit('docUpdate', jsonFromNode(doc))
  },
  { editor },
)

const editorRef = ref<HTMLDivElement | null>(null)
watchPostEffect((onCleanup) => {
  editor.mount(editorRef.value)
  onCleanup(() => editor.unmount())
})
</script>

<template>
  <ProseKit :editor="editor">
    <div class='box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 shadow dark:border-zinc-700 flex flex-col bg-white dark:bg-neutral-900'>
      <div class='relative w-full flex-1 box-border overflow-y-scroll'>
        <div ref="editorRef" class='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500 [&_pre]:text-white [&_pre]:bg-zinc-800' />
      </div>
    </div>
  </ProseKit>
</template>

useEditor

Retrieves the current editor instance within a ProseKit component.

ts
const editor = useEditor()

If you pass { update: true }, it will trigger a re-render when the editor state changes.

ts
const editor = useEditor({ update: true })

This is useful if you want to update the UI based on the current editor state. For example, you can calculate the word count of the document after every change. Check out word-counter for a complete implementation.

useExtension

Adds an extension to the editor.

ts
const extension = computed(() => defineMyExtension())
useExtension(extension)

useKeymap

Adds key bindings to the editor.

ts
import type { Keymap } from 'prosekit/core'
import { useKeymap } from 'prosekit/vue'
import { computed, type Ref } from 'vue'

export function useSubmitKeymap(
  hotkey: Ref<'Shift-Enter' | 'Enter'>,
  onSubmit: (hotkey: string) => void,
) {
  const keymap: Ref<Keymap> = computed(() => {
    return {
      [hotkey.value]: () => {
        onSubmit(hotkey.value)
        // Return true to stop further keypress propagation.
        return true
      },
    }
  })

  useKeymap(keymap)
}

Check out keymap for a complete implementation.

defineVueNodeView

Renders a node using a Vue component.

In some cases, Vue might be a more convenient tool for implementing certain interactions. For instance, for a code block, you might want to add a language selector that lets you change the language of the code block. You can implement this using a Vue component.

We begin by creating a CodeBlockView component to render the node. This component receives VueNodeViewProps as props, which include the node and other useful details.

vue
<script setup lang="ts">
import type { CodeBlockAttrs } from 'prosekit/extensions/code-block'
import { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'
import type { VueNodeViewProps } from 'prosekit/vue'
import { computed } from 'vue'

const props = defineProps<VueNodeViewProps>()

const language = computed({
  get() {
    const attrs = props.node.value.attrs as CodeBlockAttrs
    return attrs.language || ''
  },
  set(language: string) {
    const attrs: CodeBlockAttrs = { language }
    props.setAttrs(attrs)
  },
})
</script>

<template>
  <div class='relative left-2 top-3 h-0 select-none overflow-visible' contenteditable="false">
    <select v-model="language" class='outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded border-none bg-transparent px-2 py-1 text-xs transition text-white opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 [div[data-node-view-root]:hover_&]:hover:opacity-80'>
      <option value="">Plain Text</option>
      <option
        v-for="info of shikiBundledLanguagesInfo"
        :key="info.id"
        :value="info.id"
      >
        {{ info.name }}
      </option>
    </select>
  </div>
  <pre :ref="props.contentRef" :data-language="language" />
</template>

CodeBlockView renders a LanguageSelector component (the button in the top left corner) and a <pre> element to hold the code. We bind the contentRef to the <pre> element, which allows the editor to manage its content.

After defining the component, we can register it as a node view using defineVueNodeView. The name is the node's name, in this case "codeBlock". contentAs is the property name that contains the node's content. In this case, it's "code", which means a <code> element will be rendered inside the <pre> element. component is the component we just defined.

ts
import { 
defineVueNodeView
, type
VueNodeViewComponent
} from 'prosekit/vue'
import
CodeBlockView
from './code-block-view.vue'
const
extension
=
defineVueNodeView
({
name
: 'codeBlock',
contentAs
: 'code',
component
:
CodeBlockView
as
VueNodeViewComponent
,
})

Check out code-block for a complete implementation.