3

I am trying to embed a Codemirror instance as a Vue component, similar to what was accomplished in this project. However, instead of returning a template of the Codemirror instance inside of a parent

<div class="vue-codemirror" :class="{ merge }">
    <!-- Textarea will be replaced by Codemirror instance. -->
    <textarea ref="textarea" :name="name" :placeholder="placeholder" v-else>
    </textarea>
</div>

I am simplying trying to return the Codemirror instance alone. The reason why I can't simply remove the parent div and have

<!-- Textarea will be replaced by Codemirror instance. -->
<textarea ref="textarea" :name="name" :placeholder="placeholder" v-else>
</textarea>

is because the method to replace the textarea, CodeMirror.fromTextArea(), requires the textarea to have a parentNode. Otherwise, a null error will be encountered.

Luckily, there is a way to create a Codemirror instance without using a textarea at all, CodeMirror(). This instance has a function getWrapperElement() that returns the DOM node:

<div class="CodeMirror cm-s-default">
   ...
</div>

I want to output this specific DOM node relating to Codemirror instance using the Vue template/render function. The current way I'm creating the instance is by initializing it in the component data object.

data: function () {
    // Create a CodeMirror instance.
    let cm = new CodeMirror(null, {
        ...
    })
    return {
        // Define a CodeMirror instance for each CodeBlockView.
        cm : cm,
        ...
    }
},

Update 1: I have found one way to do this, albeit very hacker-ish. We use the createElement function that is passed in with render and pass into the data object argument the innerHTML of DOM node we want to render. It seems to be working visually but the CodeMirror instance isn't editable.

render: function (createElement) {
    return createElement('div', {
        class: this.dom.classList.value,
        domProps: {innerHTML: this.dom.innerHTML}
    })
}

Update 2: However, this doesn't pass in the actual DOM node rather it only shallowly copies it; this presents a problem as it doesn't allow it to be editable nor have a CodeMirror instance attached.

4
  • It seems you need to hide textarea instead of removing completely: <textarea ref="textarea" :name="name" :placeholder="placeholder" v-show="!merge"></textarea> Commented Aug 25, 2019 at 6:11
  • The removal of the textarea is done from within the CodeMirror library; I'd prefer not to have to change the original code of that. Is there a way to use the DOM node from getWrapperElement as the template to render? Commented Aug 25, 2019 at 7:22
  • Using render is a wrong way to do what you want. Prefer using ref instead vuejs.org/v2/api/#ref Commented Aug 25, 2019 at 10:05
  • Right now, I have as the template: <textarea ref="textarea"></textarea>, and the CodeMirror instantiation method is in mounted. Based off what I've read on vuejs.org/v2/guide/instance.html, it seems that this.$refs.textarea.parentNode shouldn't be null in mounted as this.$refs.textarea should be on the actual DOM node. For some reason this leads to a continuous cycle between mounted and created, crashing my browser window. Commented Aug 25, 2019 at 10:41

2 Answers 2

1

You cannot change the template anymore after the component has been created. It's not a property of the component but a parameter that is passed to the constructor as far as I know.

Hence, you would need to create the CodeMirror instance before the component is created and inject it.

However, I fail to see the problem with a wrapping component, could you explain the why?

Sign up to request clarification or add additional context in comments.

3 Comments

What do you mean by injecting the instance? I'd prefer a no-wrapping component as I am trying to replicate what was done in prosemirror.net/examples/codemirror, with the contenteditable="false" and CodeMirror on the same div, mainly to avoid later writing something like div.childElement.CodeMirror.focus(). I'm pretty new to Vue but avoiding adding another div I hope would be simple.
Hm, I have found that setting this.$el = this.CodeMirror.getWrapperElement() in mounted seems to replace the textarea and have that render outwards.
I didn't know that the $el reference is settable! Interesting. But might be that replacing the DOM element with another one has some negative implications somewhere. But I am not sure whether if it is preferable to replace the elements over referencing an element by id/class.
1

I test many ways and the most simple template I find was keeping the textarea, after that the component should receive options and value and emit the new value.

you can test the component here

I'm not a codeMirror expert, I import many things for keep the codemirror component more flexible, you can adjust the imports with your needs.

//component codeMirror.vue
<template>
  <textarea ref="myCm"></textarea>
</template>

<script>
import CodeMirror from 'codemirror';

// language
import 'codemirror/mode/javascript/javascript';
// theme css
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/monokai.css';
// require active-line.js
import 'codemirror/addon/selection/active-line';
// styleSelectedText
import 'codemirror/addon/selection/mark-selection';
import 'codemirror/addon/search/searchcursor';
// hint
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/javascript-hint';
// highlightSelectionMatches
import 'codemirror/addon/scroll/annotatescrollbar';
import 'codemirror/addon/search/matchesonscrollbar';

import 'codemirror/addon/search/match-highlighter';
// keyMap
import 'codemirror/mode/clike/clike';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/comment/comment';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/dialog/dialog.css';

import 'codemirror/addon/search/search';
import 'codemirror/keymap/sublime';
// foldGutter
import 'codemirror/addon/fold/foldgutter.css';
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/addon/fold/comment-fold';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/indent-fold';
import 'codemirror/addon/fold/markdown-fold';
import 'codemirror/addon/fold/xml-fold';

export default {
  name: 'codeMirror',
  props: {
    value: String, // give to the component a start value
    options: Object, // give the codeMirror options
  },
  mounted() {
    const myCodemirror = new CodeMirror.fromTextArea(this.$refs.myCm, this.options);
    myCodemirror.setValue(this.value);
    myCodemirror.on('change', (cm) => {
      if (this.$emit) {
        this.$emit('input', cm.getValue());
      }
    });
  },
};

</script>

after that is easy to use this component for render codeMirror in multiple instances, readOnly, recovery de data, multiple templates, no limits. ex:

<template>
  <div>
    Read Only, default theme, value multiple line
    <codeMirror :value="value0" :options="noChanges"></codeMirror>
    Value Dynamic, theme monokai, other extra fancy things
    <codeMirror :value="value" :options="monokai" @input="onCmCodeChange"></codeMirror>
    Only for debug, shows the modifications made inside codemirror
    {{ value }}
  </div>
</template>

<script>
// @ is an alias to /src
import codeMirror from '@/components/codeMirror.vue';

export default {
  name: 'pageEditor',
  components: {
    codeMirror,
  },
  data() {
    return {
      value0: `let choco: "bombon";
        let type: 'caramel;
        choco + '  ' + type;`,
      value: 'let frankie= "It\'s alive"',
      noChanges: {
        theme: 'default',
        readOnly: true,
        tabSize: 2,
        line: true,
        lineNumbers: true,
      },
      monokai: {
        tabSize: 2,
        styleActiveLine: true,
        lineNumbers: true,
        line: true,
        foldGutter: true,
        styleSelectedText: true,
        mode: 'text/javascript',
        keyMap: 'sublime',
        matchBrackets: true,
        showCursorWhenSelecting: true,
        theme: 'monokai',
        extraKeys: { Ctrl: 'autocomplete' },
        hintOptions: {
          completeSingle: false,
        },
      },

    };
  },
  methods: {
    onCmCodeChange(newValue) {
      this.value = newValue;
    },
  },
};
</script>

I hope this help

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.