blog bg

May 29, 2024

Building a Simple Code Editor with Vue.js and CodeMirror

Share what you learn in this blog to prepare for your interview, create your forever-free profile now, and explore how to monetize your valuable knowledge.

Introduction
In this tutorial, we'll explore how to create a simple code editor using Vue.js and CodeMirror. Code editors are essential tools for developers, providing syntax highlighting, line numbering, and other features to enhance coding experience. We'll leverage Vue.js to build a user-friendly interface and integrate CodeMirror to enable code editing functionalities.

 


 

Understanding the Need

When working on projects like cloning pastebin.com, developers often encounter the challenge of providing a user-friendly code editing interface. Traditional text editors lack essential features like syntax highlighting and can be cumbersome for users. Hence, integrating a code editor within a web browser becomes crucial to enhance user experience and productivity.

 

Leveraging CodeMirror

CodeMirror is a powerful tool used by various companies and platforms like CodePen for syntax highlighting and code editing functionalities. We'll integrate CodeMirror with Vue.js to create a feature-rich code editor similar to popular IDEs like Sublime Text or Visual Studio Code.

 

Setting Up the Environment

First, we'll install the necessary package, `vue3-codemirror`, using npm. This package provides seamless integration of CodeMirror with Vue.js. Once installed, we'll import and configure it within our Vue component.
 

 

 

 

 

npm i vue3-codemirror

 

Implementing the Code Editor

We'll create a Vue component and incorporate the CodeMirror instance within its template. Configuring properties like `mode` (language), `value` (initial code), and other options will enable us to customize the editor's behavior according to our requirements.
 

 

 

 

 

var VCodeMirror_1;
import { __decorate, __metadata } from "tslib";
import CodeMirror from 'codemirror';
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/foldgutter.css';
import 'codemirror/mode/python/python';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/dracula.css';
import 'codemirror/theme/abcdef.css';
import { capitalize, h, markRaw } from 'vue';
import ResizeObserver from 'resize-observer-polyfill';
import { $theme } from 'theme-helper';
import { Component, Inreactive, Prop, VueComponentBase, Watch } from 'vue3-component-base';
const Events = [
    'focus',
    'blur',
    'scroll',
];

let VCodeMirror = VCodeMirror_1 = class VCodeMirror extends VueComponentBase {
    render() {
        return h('div', { class: 'v-code-mirror' });
    }
    async mounted() {


        const lang_mode=this.$props.mode.name

        
        
        if(lang_mode!=='plaintext' && lang_mode !== null && lang_mode !== "null"){
            

            await import(`codemirror/mode/${lang_mode}/${lang_mode}`)

            
        }
        
                          

        var editor = this.editor = markRaw(CodeMirror(this.$el, {
            value: this.value,
            mode: this.$props.mode,
            theme: $theme.get() == 'white' ? 'default' : 'dracula',
            readOnly: this.readonly,
            lineWrapping: this.wrap,
            lineNumbers: true,
            foldGutter: true,
            gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
            ...this.options,
        }));
        editor.on('changes', () => {
            const value = editor.getValue();
            this.backupValue = value;
            this.$emit('update:value', editor.getValue());
        });
        Events.forEach(x => {
            const eventName = 'on' + capitalize(x);
            if (typeof this.$.vnode.props[eventName] === 'function') {
                editor.on(x, this.$emit.bind(this, x));
            }
        });
        this.cleanEvent = markRaw($theme.onchange(({ detail }) => {
            this.editor.setOption('theme', detail === 'white' ? 'default' : 'dracula');
        }));
        this.backupValue = this.value;
        this.$el._component = this;
        if (!VCodeMirror_1.ro) {
            VCodeMirror_1.ro = new ResizeObserver(function (entries) {
                entries.forEach(entry => {
                    const that = entry.target._component;
                    if (that.autoHeight) {
                        that.editor.refresh();
                    }
                    else {
                        that.editor.setSize(entry.contentRect.width, entry.contentRect.height);
                    }
                });
            });
        }
        VCodeMirror_1.ro.observe(this.$el);
        
    }
    async updated(){
        
        if(this.$props.mode.name){

            const lang_mode=this.$props.mode.name

            if(lang_mode!=='plaintext' && lang_mode !== null && lang_mode !== "null"){
            

                await import(`codemirror/mode/${lang_mode}/${lang_mode}`)
    
                
            }
          
        }

        this.editor.setOption('mode',this.$props.mode)
       
        
        
    }
    beforeUnmount() {
        var _a, _b;
        (_a = this.cleanEvent) === null || _a === void 0 ? void 0 : _a.call(this);
        (_b = VCodeMirror_1.ro) === null || _b === void 0 ? void 0 : _b.unobserve(this.$el);
    }
    updateValue(value) {
        if (value === this.backupValue)
            return;
        this.editor.setValue(value);
    }
    updateReadonly(value) {
        this.editor.setOption('readOnly', value);
    }
    updateWrap(value) {
        this.editor.setOption('lineWrapping', value);
    }
    focus() {
        this.editor.focus();
    }
};
__decorate([
    Prop({ required: true }),
    __metadata("design:type", String)
], VCodeMirror.prototype, "value", void 0);
__decorate([
    Prop({ default: () => ({ name: 'python', json: true }) }),
    __metadata("design:type", Object)
], VCodeMirror.prototype, "mode", void 0);
__decorate([
    Prop(),
    __metadata("design:type", Boolean)
], VCodeMirror.prototype, "readonly", void 0);
__decorate([
    Prop({ default: true }),
    __metadata("design:type", Boolean)
], VCodeMirror.prototype, "wrap", void 0);
__decorate([
    Prop(),
    __metadata("design:type", Object)
], VCodeMirror.prototype, "options", void 0);
__decorate([
    Prop(),
    __metadata("design:type", Boolean)
], VCodeMirror.prototype, "autoHeight", void 0);
__decorate([
    Inreactive,
    __metadata("design:type", Object)
], VCodeMirror.prototype, "editor", void 0);
__decorate([
    Inreactive,
    __metadata("design:type", String)
], VCodeMirror.prototype, "backupValue", void 0);
__decorate([
    Inreactive,
    __metadata("design:type", Function)
], VCodeMirror.prototype, "cleanEvent", void 0);
__decorate([
    Watch('value'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String]),
    __metadata("design:returntype", void 0)
], VCodeMirror.prototype, "updateValue", null);
__decorate([
    Watch('readonly'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Boolean]),
    __metadata("design:returntype", void 0)
], VCodeMirror.prototype, "updateReadonly", null);
__decorate([
    Watch('wrap'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Boolean]),
    __metadata("design:returntype", void 0)
], VCodeMirror.prototype, "updateWrap", null);
VCodeMirror = VCodeMirror_1 = __decorate([
    Component({
        name: 'VCodeMirror',
        emits: ['update:value', ...Events],
    })
], VCodeMirror);
export { VCodeMirror };
//# sourceMappingURL=index.js.map

Enhancing User Experience

We'll explore additional options provided by CodeMirror, such as adjusting tab size, enabling line numbers, and more. These options allow us to tailor the editor to meet specific preferences and improve usability.
 

 

 

 

 

<template>
  <div class="min-h-screen px-5 md:px-20 pt-10 flex md:flex-row flex-col ">
    <div class=" w-full md:w-10/12">

      <h2 class='font-medium text-2xl'>
        New Paste
      </h2>
      <hr class='border-gray-500 '>

      <div :class="!this.$store.getters.getUserPastePrefence.nightMode?'bg-gray-200 ':'bg-gray-700' " class=' rounded-lg p-2 my-2' v-if='error'>
            <p class='text-red-400  text-sm my-1'  >{{error}}</p>

      </div>

      <VCodeMirror @update:value='handleUpdate' 
                    :options='{...cmOptions}' 
                    value='' 
                    :mode='mode'
                    :readonly='false'
                    
                    
                    class='w-full shadow-xl my-5 bg-gray-700 h-96 resize-none rounded-lg outline-none  text-base' />
    
      <h2 class='font-semibold text-xl'>
        Optional Paste Setting
      </h2>
      <hr class='border-gray-500 '>
      <form @submit.prevent='handleSubmit' >

        <AppSelect @selectChange='changeDefaultLang' :value='defaultLang' :selectOption='languages' title='Syntax highlighting' />
        <AppSelect @selectChange='changeDefaultExpire' :value='defaultExpire' :selectOption='expirations' title='Paste Expiration' />
        <AppSelect @selectChange='changeDefaultPrivacy' :value='defaultPrivacy' :selectOption='["Public","Private"]' title='Paste Exposure' />
        <PasteFolderTitle :value="defaultFolder" :openNewFolder='openNewFolder'  @newFolder='handleNewFolder' @selectChange='handlePasteFolder' />
        <AppInput title='Paste Name / Title' placeholde='Paste title' inputType='text' @InputChange='changePasteTitle' />
        <CreatePastePasswordInput title='Password' @passwordChange='handlePassword' />
        <div class='flex items-center my-5'>
          <button class=' py-2 px-5 font-medium rounded-md mr-4'
          :class="!this.$store.getters.getUserPastePrefence.nightMode?'bg-gray-200 text-gray-800':'bg-gray-700' ">
            Create New Paste
          </button>
          <div class='flex items-center' v-if='isUser' >

              <input type="checkbox" v-model='guestPost'  class="p-2 mr-2 bg-gray-600 ">
              <label class="text-lg">Paste as a guest</label>
              
          </div>
        </div>
      </form>

    </div>

    <div class='md:w-2/12 w-full p-2 pb-10 md:pb-0'>
      <PasteList class='ml-4' style='margin-top:0;' />
    </div>
  </div>
</template>

<script>

import PasteList from '../components/PasteList.vue'
import AppSelect from '../components/AppSelect.vue'
import AppInput from '../components/AppInput.vue'
import CreatePastePasswordInput from '../components/CreatePastePasswordInput.vue'
import CreatePasteTitleInput from '../components/CreatePasteTitleInput.vue'
import PasteFolderTitle from '../components/PasteFolderTitle.vue'
import { VCodeMirror } from '../components/vue3-code-mirror'
import {ref,computed, onBeforeMount,watch} from 'vue'
import handleNewPaste from '@/composibles/handleNewPaste'

import { useRouter } from 'vue-router'
import {useStore} from 'vuex'

export default {
    components:{    
        PasteList,
        VCodeMirror,
        AppSelect,
        AppInput,
        PasteFolderTitle,
        CreatePastePasswordInput,
        CreatePasteTitleInput,
    },
    setup(){
      const code= ref('')
      const defaultLang=ref('PLAINTEXT')
      const defaultExpire=ref('Never')
      const defaultPrivacy=ref('Public')
      const defaultFolder=ref('no folder selected')
      const openNewFolder=ref(false)
      const newFolder=ref('')
      const pasteTitle=ref('')
      const pastePassword=ref('')
      const guestPost=ref(false)

      const store=useStore()
      const userPreference=computed(()=>{
        return store.getters.getUserPastePrefence
      })

      onBeforeMount(async()=>{

        // await store.dispatch('handleChangeUserPreference')

       

        defaultLang.value=userPreference.value.defaultSyntax
        defaultExpire.value=userPreference.value.defaultExpiration
        defaultPrivacy.value=userPreference.value.defaultExposure?'Public':'Private'




      })

      const languages=computed(()=>{
        return store.getters.getLanguages
      })

      const expirations=computed(()=>{
        return store.getters.getExpirations
      })

      const isDark=computed(()=>{
        return store.getters.getUserPastePrefence.nightMode ? "dracula" : "white"
      })
      

      const router =useRouter()

      const {error,postToAPI}=handleNewPaste()

      const isUser=computed(()=>{
        return store.getters.getUser
      })

      const handleSubmit=async()=>{

        await store.dispatch('handleChangeUser')

        let withAuth = guestPost.value ? false : isUser 

        if(guestPost.value){

          defaultFolder.value='no folder selected'
          newFolder.value=''

        }


        let data
        
        if(defaultFolder.value==='no folder selected'  && newFolder.value.length){

          const pasteObj={
            code:code.value,
            language:defaultLang.value.toUpperCase(),
            expiration:defaultExpire.value.toUpperCase(),
            public:defaultPrivacy.value=='Public'? true : false ,
            syntax_highlighting:true,
            folder:newFolder.value,
            password:pastePassword.value,
            title:pasteTitle.value,
          }
          
          data=await postToAPI(pasteObj,withAuth)

        }
        else if(defaultFolder.value!=='no folder selected'){
          const pasteObj={
            code:code.value,
            language:defaultLang.value.toUpperCase(),
            expiration:defaultExpire.value.toUpperCase(),
            public: defaultPrivacy.value=='Public'? true : false ,
            folder:defaultFolder.value,
            password:pastePassword.value,
            title:pasteTitle.value,
            syntax_highlighting:true
          }
          data=await postToAPI(pasteObj,withAuth)

        }else{
          const pasteObj={
            code:code.value,
            language:defaultLang.value.toUpperCase(),
            expiration:defaultExpire.value.toUpperCase(),
            public:defaultPrivacy.value=='Public'? true : false ,
            password:pastePassword.value,
            title:pasteTitle.value,
            syntax_highlighting:true
            
          }
          console.log(pasteObj)
          data=await postToAPI(pasteObj,withAuth)
        }
        
        if(error.value===null){
          router.push(`/paste/${data.uuid}`)
        }

      }
      
      const cmOptions= ref({
        tabSize: 4,
        lineNumbers: true,
        // line: true,
        theme: isDark.value
      })

      watch(isDark,()=>{
        cmOptions.value= ref({
        tabSize: 4,
        lineNumbers: true,
        // line: true,
        theme: isDark.value
      })
      })

      const handleUpdate=(value)=>{
        // console.log(value)
        code.value=value
        
      }

      


      const mode= ref({name:defaultLang.value=='PLAINTEXT'?'null':'python',json:true})

      const changeDefaultLang=(value)=>{
        defaultLang.value=value
        mode.value={
          name: value=='PLAINTEXT'?'null':value.toLowerCase(),json:true
        }
        console.log(defaultLang.value)
      }
      const changeDefaultExpire=(value)=>{
        defaultExpire.value=value
        console.log(defaultExpire.value)
      }
      const changeDefaultPrivacy=(value)=>{
        defaultPrivacy.value=value
        console.log(defaultPrivacy.value)
      }

      const handleNewFolder=(value)=>{
        newFolder.value=value
        console.log(defaultFolder.value)
        console.log(newFolder.value)
      }
      const handlePasteFolder=(value)=>{
        defaultFolder.value=value
        console.log(defaultFolder.value)

      }

      const changePasteTitle=(title)=>{
        pasteTitle.value=title
        console.log(pasteTitle.value)
      }

      const handlePassword=(value)=>{
        pastePassword.value=value
        console.log(pastePassword.value)
      }

      


      return {code,cmOptions,handleUpdate,mode,defaultLang,
              defaultExpire,
              defaultPrivacy,
              defaultFolder,
              changeDefaultLang,
              changeDefaultExpire,
              changeDefaultPrivacy,
              handleNewFolder,
              handlePasteFolder,
              handlePassword,
              openNewFolder,
              changePasteTitle,
              handleSubmit,
              error,
              languages,
              expirations,
              isUser,
              guestPost}
    }
    
}
</script>

<style>
  .CodeMirror-vscrollbar::-webkit-scrollbar{
    width:10px;
    background: rgba(128, 128, 128, 0.644);
    border-radius:0.5rem;

  }

  .CodeMirror-vscrollbar::-webkit-scrollbar-thumb{
    width:2px;
    background:rgba(0, 0, 0, 0.678);
    border-radius:32px
  }

  .CodeMirror-vscrollbar{
    width:10px;
    background: rgba(128, 128, 128, 0.644);
    border-radius:0.5rem;
  }

  .CodeMirror{
    border-radius: 0.5rem;
  }
</style>

 

Handling User Input and Interaction

CodeMirror emits events whenever there's a change in the code editor. We'll utilize these events to capture user input and update the code accordingly. Additionally, we'll implement features like read-only mode to restrict user editing and ensure data integrity.

 

Conclusion

By integrating CodeMirror with Vue.js, we've successfully created a simple yet powerful code editor within a web browser. This tutorial demonstrates the versatility and ease of use provided by modern web development frameworks and libraries. Experiment with different configurations and functionalities to customize the code editor according to your project's needs.

Remember to subscribe to my channel for more tutorials on web development with Python, Go, Vue.js, React, and more. If you found this tutorial helpful, don't forget to like and share it. Feel free to leave comments with any questions or project requests, and we'll be glad to assist you. Happy coding!

322 views

Please Login to create a Question