DEV Community

Casual Coder
Casual Coder

Posted on

Rich text editor for Vue using Tiptap and Vuetify

Recently, I wanted to add a rich text editor in one of my pet project. I dabbled with CKEditor for a while but I found it a bit difficult to adopt in Vue (I'm fairly new to front end development)
Other option was Tiptap, It is a renderless editor based on Prosemirror. It was easy to follow, I was able to create a basic editor without any style quickly.

This is an overview of how to use Vuetify for styling of Tiptap editor

Setup

Assuming you alreaday have a Vuetify project-

$ npm add tiptap tiptap-extensions 

Code Overview

Key concepts before going into the code (from docs)

Editor class

This class is a central building block of tiptap. It does most of the heavy lifting of creating a working ProseMirror editor such as creating the EditorView, setting the initial EditorState and so on. The Editor constructor accepts an object of editor options.

EditorContent

This is like an container component which accepts Editor instance as a property.

Extensions

Each editor feature like Headings, Bold, Italics, Images etc. are implemented as extensions. We need to pass instance of each extension in the editor option for each of the feature we want in our editor.

EditorMenuBar

Alt Text

This component holds all the toolbar buttons. The action is performed through commands eg. commands.bold, commands.image, which can be linked to click event of any button.

With this background we can dive into code -

Add Editor instance with Heading, Bold, Underline and Image extensions.

data() {
    return {
      editor: new Editor({
        content: `Type here...
        `,
        extensions:[
            new Heading({levels: [1,2,3]}),
            new Bold(),
            new Underline(),
            new Image(),
        ]
      })
    }
  },

Pass editor as property to editor-content component

 <editor-content class="editor-box" :editor="editor"/>

Create Editor's menubar. It uses slots which I don't fully understand at this point, But all we need to understand is that commands are the actions that we wish to perform like making something Bold, inserting image and isActive is used to check if current line or current selection has the Bold/Italic or not.

<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
            <div>
                <v-btn text icon
                :class="{ 'is-active': isActive.bold() }"
                @click="commands.bold"
                >
                    <v-icon>mdi-format-bold</v-icon>
                </v-btn>
            </div>
</editor-menu-bar>

Here's the full code of the view.

<template>
  <v-container>
      <v-row>
        <editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
            <div >
                <v-btn text icon
                :class="{ 'is-active': isActive.heading({ level: 1 }) }"
                @click="commands.heading({level: 1})"
                >
                   <b> H1 </b>
                </v-btn>
                <v-btn text icon
                :class="{ 'is-active': isActive.bold() }"
                @click="commands.bold"
                >
                    <v-icon>mdi-format-bold</v-icon>
                </v-btn>

                <v-btn text icon
                :class="{ 'is-active': isActive.underline() }"
                @click="commands.underline"
                >
                    <v-icon>mdi-format-underline</v-icon>
                </v-btn>

               <v-btn text icon
               @click="loadImage(commands.image)">
                   <v-icon>mdi-image</v-icon>
               </v-btn>
            </div>            
        </editor-menu-bar>
    </v-row>
    <v-row>
        <v-col cols=12 >
            <editor-content class="editor-box" :editor="editor"/>
        </v-col>
    </v-row>
  </v-container>
</template>


<script>
import { Editor, EditorContent, EditorMenuBar  } from 'tiptap';
import { Heading, 
        Bold, 
        Underline,
        Image } from 'tiptap-extensions';
export default {
components: {
    EditorContent,
    EditorMenuBar,
  },
  data() {
    return {
      editor: new Editor({
        content: `Type here...
        `,
        extensions:[
            new Heading({levels: [1,2,3]}),
            new Bold(),
            new Underline(),
            new Image(),
        ]
      })
    }
  },
  methods:{
      loadImage:function(command){
          command({src: "https://66.media.tumblr.com/dcd3d24b79d78a3ee0f9192246e727f1/tumblr_o00xgqMhPM1qak053o1_400.gif"})
      }
  },
  beforeDestroy() {
    this.editor.destroy()
  },
};
</script>
<style >
.editor-box> * {
    border-color: grey;
    border-style: solid;
    border-width: 1px;
}

.is-active{
    border-color: grey;
    border-style: solid;
    border-width: 1px;
}
 /* *:focus {
    outline: none;
}  */
</style>

Here's how it looks ultimately-

Alt Text

Hope this helps

Top comments (8)

Collapse
 
iliyazelenko profile image
Илья

Good article. You can also use my package tiptap-vuetify for quick start.

Collapse
 
nomikz profile image
nomikz

I've checked out you github well done)

Collapse
 
aminemshady2080 profile image
aminem2080

wow, what an easy to user plugin
congs man

Collapse
 
geoabdrabuh profile image
mohamed Abdrabu

hi, many thanks for the awesome tool, but please how can I write the content to json file because it give null value when i try to write the content in json file

let jsonData = {discussion:this.discussion}

Collapse
 
angelguerrero profile image
Ángel Guerrero

Awesome tool, i was searching something like that! Thanks!!! :)

Collapse
 
nomikz profile image
nomikz

How to get the content of the editor?
Is it in this.editor? This part is confusing me. Because this.editor doesn't contain generated html content.

Collapse
 
nomikz profile image
nomikz

I've found out how to do that
onUpdate: ({ getHTML }) => {
// get new content on update
const newContent = getHTML()
},

Collapse
 
geoabdrabuh profile image
mohamed Abdrabu

would you please explain to me how to write the content from the text editor to json file because it gives null values