> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/ueberdosis/tiptap/llms.txt
> Use this file to discover all available pages before exploring further.

# Vue 2

> Learn how to integrate Tiptap with Vue 2

Tiptap provides support for Vue 2 applications. While Vue 2 has reached end-of-life, this integration allows legacy projects to use Tiptap.

<Note>
  Vue 2 reached end-of-life on December 31, 2023. Consider upgrading to Vue 3 for new projects. This package is maintained for existing Vue 2 applications.
</Note>

## Installation

<Steps>
  <Step title="Install the packages">
    Install the Vue 2 package along with the core package and any extensions you need:

    ```bash theme={null}
    npm install @tiptap/vue-2 @tiptap/core @tiptap/starter-kit
    ```
  </Step>

  <Step title="Import and use">
    Import the necessary components in your Vue component:

    ```vue theme={null}
    <script>
    import { Editor, EditorContent } from '@tiptap/vue-2'
    import StarterKit from '@tiptap/starter-kit'
    </script>
    ```
  </Step>
</Steps>

## Core Concepts

### Editor Class

In Vue 2, you create an editor instance using the `Editor` class directly. The editor should be created in the `mounted` lifecycle hook and destroyed in `beforeDestroy`.

#### Basic Usage

```vue theme={null}
<template>
  <div v-if="editor">
    <editor-content :editor="editor" />
  </div>
</template>

<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
    }
  },

  mounted() {
    this.editor = new Editor({
      extensions: [StarterKit],
      content: '<p>Hello World!</p>',
    })
  },

  beforeDestroy() {
    this.editor.destroy()
  },
}
</script>
```

### EditorContent Component

The `EditorContent` component renders the actual editor interface.

```vue theme={null}
<template>
  <div class="editor-wrapper">
    <editor-content 
      :editor="editor" 
      class="editor"
    />
  </div>
</template>

<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
    }
  },

  mounted() {
    this.editor = new Editor({
      extensions: [StarterKit],
      content: '<p>Hello World!</p>',
    })
  },

  beforeDestroy() {
    this.editor.destroy()
  },
}
</script>

<style scoped>
.editor-wrapper {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 1rem;
}
</style>
```

## Complete Working Examples

<CodeGroup>
  ```vue Basic Editor theme={null}
  <template>
    <div v-if="editor" class="container">
      <editor-content :editor="editor" />
    </div>
  </template>

  <script>
  import { Editor, EditorContent } from '@tiptap/vue-2'
  import StarterKit from '@tiptap/starter-kit'

  export default {
    components: {
      EditorContent,
    },

    data() {
      return {
        editor: null,
      }
    },

    mounted() {
      this.editor = new Editor({
        extensions: [StarterKit],
        content: `
          <h2>Hi there,</h2>
          <p>this is a <em>basic</em> example of <strong>Tiptap</strong>.</p>
        `,
      })
    },

    beforeDestroy() {
      this.editor.destroy()
    },
  }
  </script>
  ```

  ```vue With Toolbar theme={null}
  <template>
    <div v-if="editor" class="container">
      <div class="menu-bar">
        <button
          @click="editor.chain().focus().toggleBold().run()"
          :disabled="!editor.can().chain().focus().toggleBold().run()"
          :class="{ 'is-active': editor.isActive('bold') }"
        >
          Bold
        </button>
        <button
          @click="editor.chain().focus().toggleItalic().run()"
          :disabled="!editor.can().chain().focus().toggleItalic().run()"
          :class="{ 'is-active': editor.isActive('italic') }"
        >
          Italic
        </button>
        <button
          @click="editor.chain().focus().toggleStrike().run()"
          :disabled="!editor.can().chain().focus().toggleStrike().run()"
          :class="{ 'is-active': editor.isActive('strike') }"
        >
          Strike
        </button>
        <button
          @click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
          :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
        >
          H1
        </button>
        <button
          @click="editor.chain().focus().toggleBulletList().run()"
          :class="{ 'is-active': editor.isActive('bulletList') }"
        >
          Bullet List
        </button>
        <button
          @click="editor.chain().focus().undo().run()"
          :disabled="!editor.can().chain().focus().undo().run()"
        >
          Undo
        </button>
        <button
          @click="editor.chain().focus().redo().run()"
          :disabled="!editor.can().chain().focus().redo().run()"
        >
          Redo
        </button>
      </div>
      
      <editor-content :editor="editor" />
    </div>
  </template>

  <script>
  import { Editor, EditorContent } from '@tiptap/vue-2'
  import StarterKit from '@tiptap/starter-kit'

  export default {
    components: {
      EditorContent,
    },

    data() {
      return {
        editor: null,
      }
    },

    mounted() {
      this.editor = new Editor({
        extensions: [StarterKit],
        content: `
          <h2>Hi there,</h2>
          <p>this is a <em>basic</em> example of <strong>Tiptap</strong>.</p>
        `,
      })
    },

    beforeDestroy() {
      this.editor.destroy()
    },
  }
  </script>

  <style scoped>
  .menu-bar {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1rem;
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
  }

  button {
    padding: 0.5rem 1rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    background: white;
    cursor: pointer;
  }

  button:hover {
    background: #f5f5f5;
  }

  button.is-active {
    background: #333;
    color: white;
  }

  button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
  </style>
  ```

  ```vue Minimal theme={null}
  <template>
    <editor-content :editor="editor" />
  </template>

  <script>
  import Document from '@tiptap/extension-document'
  import Paragraph from '@tiptap/extension-paragraph'
  import Text from '@tiptap/extension-text'
  import { Editor, EditorContent } from '@tiptap/vue-2'

  export default {
    components: {
      EditorContent,
    },

    data() {
      return {
        editor: null,
      }
    },

    mounted() {
      this.editor = new Editor({
        extensions: [Document, Paragraph, Text],
        content: '<p>This is a minimal Tiptap editor.</p>',
      })
    },

    beforeDestroy() {
      this.editor.destroy()
    },
  }
  </script>
  ```
</CodeGroup>

## Event Handlers

Handle editor events through the editor options:

```vue theme={null}
<template>
  <div>
    <editor-content :editor="editor" />
    <div>HTML Output: {{ html }}</div>
  </div>
</template>

<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
      html: '',
    }
  },

  mounted() {
    const vm = this
    
    this.editor = new Editor({
      extensions: [StarterKit],
      content: '<p>Hello World!</p>',
      
      onUpdate({ editor }) {
        vm.html = editor.getHTML()
      },
      
      onCreate({ editor }) {
        console.log('Editor created')
      },
      
      onFocus({ editor, event }) {
        console.log('Editor focused')
      },
      
      onBlur({ editor, event }) {
        console.log('Editor blurred')
      },
    })
  },

  beforeDestroy() {
    this.editor.destroy()
  },
}
</script>
```

## Methods and Computed Properties

You can create methods and computed properties to interact with the editor:

```vue theme={null}
<template>
  <div v-if="editor">
    <div class="stats">
      <span>Characters: {{ characterCount }}</span>
      <span>Words: {{ wordCount }}</span>
    </div>
    
    <button @click="clearContent">Clear</button>
    <button @click="setContent">Set Example Content</button>
    
    <editor-content :editor="editor" />
  </div>
</template>

<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
    }
  },

  computed: {
    characterCount() {
      return this.editor?.state.doc.textContent.length || 0
    },
    
    wordCount() {
      const text = this.editor?.state.doc.textContent || ''
      return text.split(/\s+/).filter(Boolean).length
    },
  },

  methods: {
    clearContent() {
      this.editor.commands.clearContent()
    },
    
    setContent() {
      this.editor.commands.setContent('<p>This is <strong>example</strong> content!</p>')
    },
  },

  mounted() {
    this.editor = new Editor({
      extensions: [StarterKit],
      content: '<p>Hello World!</p>',
    })
  },

  beforeDestroy() {
    this.editor.destroy()
  },
}
</script>
```

## Advanced: Custom Node Views

Create Vue 2 components as custom node views:

```vue theme={null}
<!-- CustomNodeComponent.vue -->
<template>
  <node-view-wrapper class="custom-node">
    <div class="label">Custom Vue 2 Node</div>
    <node-view-content class="content" />
  </node-view-wrapper>
</template>

<script>
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'

export default {
  components: {
    NodeViewWrapper,
    NodeViewContent,
  },
}
</script>

<style scoped>
.custom-node {
  border: 2px solid #333;
  border-radius: 8px;
  padding: 1rem;
}

.label {
  font-weight: bold;
  margin-bottom: 0.5rem;
}
</style>
```

```javascript theme={null}
// CustomNodeExtension.js
import { Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-2'
import CustomNodeComponent from './CustomNodeComponent.vue'

export 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)
  },
})
```

## Menus

Bubble and floating menus work with Vue 2:

```vue theme={null}
<template>
  <div>
    <editor-content :editor="editor" />
    
    <bubble-menu :editor="editor" v-if="editor">
      <button @click="editor.chain().focus().toggleBold().run()">
        Bold
      </button>
      <button @click="editor.chain().focus().toggleItalic().run()">
        Italic
      </button>
    </bubble-menu>
    
    <floating-menu :editor="editor" v-if="editor">
      <button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">
        H1
      </button>
      <button @click="editor.chain().focus().toggleBulletList().run()">
        Bullet List
      </button>
    </floating-menu>
  </div>
</template>

<script>
import { BubbleMenu, FloatingMenu } from '@tiptap/vue-2'
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'

export default {
  components: {
    EditorContent,
    BubbleMenu,
    FloatingMenu,
  },

  data() {
    return {
      editor: null,
    }
  },

  mounted() {
    this.editor = new Editor({
      extensions: [StarterKit],
      content: '<p>Hello World!</p>',
    })
  },

  beforeDestroy() {
    this.editor.destroy()
  },
}
</script>
```

## Watchers

You can use watchers to react to editor changes:

```vue theme={null}
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
      content: '',
    }
  },

  watch: {
    content(newContent) {
      // Update editor when external content changes
      if (this.editor && this.editor.getHTML() !== newContent) {
        this.editor.commands.setContent(newContent)
      }
    },
  },

  mounted() {
    const vm = this
    
    this.editor = new Editor({
      extensions: [StarterKit],
      content: this.content,
      
      onUpdate({ editor }) {
        vm.content = editor.getHTML()
      },
    })
  },

  beforeDestroy() {
    this.editor.destroy()
  },
}
</script>
```

## Migration from Vue 2 to Vue 3

If you're upgrading from Vue 2 to Vue 3, here are the main changes:

<Tabs>
  <Tab title="Vue 2">
    ```vue theme={null}
    <script>
    import { Editor, EditorContent } from '@tiptap/vue-2'

    export default {
      data() {
        return { editor: null }
      },
      mounted() {
        this.editor = new Editor({ /* ... */ })
      },
      beforeDestroy() {
        this.editor.destroy()
      },
    }
    </script>
    ```
  </Tab>

  <Tab title="Vue 3 (Options)">
    ```vue theme={null}
    <script>
    import { Editor, EditorContent } from '@tiptap/vue-3'

    export default {
      data() {
        return { editor: null }
      },
      mounted() {
        this.editor = new Editor({ /* ... */ })
      },
      beforeUnmount() { // Changed from beforeDestroy
        this.editor.destroy()
      },
    }
    </script>
    ```
  </Tab>

  <Tab title="Vue 3 (Composition)">
    ```vue theme={null}
    <script setup>
    import { useEditor, EditorContent } from '@tiptap/vue-3'

    const editor = useEditor({ /* ... */ })
    // Cleanup is handled automatically
    </script>
    ```
  </Tab>
</Tabs>

## Next Steps

<CardGroup cols={2}>
  <Card title="Extensions" icon="puzzle-piece" href="/extensions">
    Explore available extensions to enhance your editor
  </Card>

  <Card title="Commands" icon="terminal" href="/api/commands">
    Learn about editor commands and chains
  </Card>

  <Card title="Upgrade to Vue 3" icon="arrow-up" href="/frameworks/vue-3">
    Learn about the Vue 3 integration
  </Card>

  <Card title="Node Views" icon="cube" href="/guide/node-views">
    Create interactive custom nodes with Vue components
  </Card>
</CardGroup>
