Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

VueJS and tinyMCE, custom directives

I've been struggling hard with getting VueJS and TinyMCE to work together. I've come to the conclusion that using directives would be the way to go.

So far I've been able to pass in the body as a directive parameter, and tinyMCE sets the content. However, I can't get the two way binding to work. I'm also afraid that I'm doing things completely wrong based on the tinyMCE api.

The relevant tinyMCE functions I assume would be:

http://community.tinymce.com/wiki.php/api4:method.tinymce.Editor.setContent

// Sets the content of a specific editor (my_editor in this example)
tinymce.get('my_editor').setContent(data);

and

http://community.tinymce.com/wiki.php/api4:method.tinymce.Editor.getContent

// Get content of a specific editor:
tinymce.get('content id').getContent()

HTML

<div id="app">
  <h3>This is the tinyMCE editor</h3>
  <textarea id="editor" v-editor :body="body"></textarea>

  <hr>
  <p>This input field is properly binded</p>
  <input v-model="body">

  <hr>
  <pre>data binding: {{ body }} </pre>
</div>

JS

tinymce.init({
    selector:'#editor',
});

Vue.directive('editor', {
    twoWay: true,
    params: ['body'],

    bind: function () {
        tinyMCE.get('editor').setContent(this.params.body);
        tinyMCE.get('editor').on('change', function(e) {
            alert("changed");
        });
    },
    update: function (value) {
        $(this.el).val(value).trigger('change')
    },
});

var editor = new Vue({
    el: '#app',
    data: {
        body: 'The message'
    }
})

Fiddle

https://jsfiddle.net/nf3ftm8f/

like image 873
Pistachio Avatar asked Dec 30 '15 16:12

Pistachio


2 Answers

With Vue.js 2.0, the directives are only used for applying low-level direct DOM manipulations. They don't have this reference to Vue instance data anymore. (Ref: https://v2.vuejs.org/v2/guide/migration.html#Custom-Directives-simplified)

Hence I recommend to use Component instead.

TinymceComponent:

// Use JSPM to load dependencies: vue.js 2.1.4, tinymce: 4.5.0
import Vue from 'vue/dist/vue';
import tinymce from 'tinymce';

// Local component
var TinymceComponent = {
    template: `<textarea class="form-control">{{ initValue }}</textarea>`,
    props: [ 'initValue', 'disabled' ],
    mounted: function() {
        var vm = this,
            tinymceDict = '/lib/jspm_packages/github/tinymce/[email protected]/';

        // Init tinymce
        tinymce.init({
            selector: '#' + vm.$el.id,
            menubar: false,
            toolbar: 'bold italic underline | bullist numlist',
            theme_url: tinymceDict + 'themes/modern/theme.js,
            skin_url: tinymceDict + 'skins/lightgray',
            setup: function(editor) {
                // If the Vue model is disabled, we want to set the Tinymce readonly
                editor.settings.readonly = vm.disabled;

                if (!vm.disabled) {
                    editor.on('blur', function() {
                        var newContent = editor.getContent();

                        // Fire an event to let its parent know
                        vm.$emit('content-updated', newContent);
                    });
                }
            }
        });
    },
    updated: function() {
        // Since we're using Ajax to load data, hence we have to use this hook because when parent's data got loaded, it will fire this hook.
        // Depends on your use case, you might not need this
        var vm = this;

        if (vm.initValue) {
            var editor = tinymce.get(vm.$el.id);
            editor.setContent(vm.initValue);
        }
    }
};

// Vue instance
new Vue({
    ......
    components: {
        'tinymce': TinymceComponent
    }
    ......
});

Vue Instance (simplified)

new Vue({
    el: '#some-id',
    data: {
        ......
        description: null
        ......
    },
    components: {
        'tinymce': TinymceComponent
    },
    methods: {
        ......
        updateDescription: function(newContent) {
            this.description = newContent;
        },
        load: function() {
            ......
            this.description = "Oh yeah";
            ......
        }
        ......
    },
    mounted: function() {
        this.load();
    }
});

HTML (MVC view)

<form id="some-id">
    ......
    <div class="form-group">
        <tinymce :init-value="description"
                 v-on:content-updated="updateDescription"
                 :id="description-tinymce"
                 :disabled="false">
        </tinymce>
    </div>
    ......
</form>

The flows

  1. First the data is loaded through remote resources, i.e., AJAX. The description got set.
  2. The description got passed down to the component via props: initValue.
  3. When the component is mounted, the tinymce is initialized with the initial description.
  4. It also sets up the on blur event to get the updated content.
  5. Whenever the user loses focus on the editor, a new content is captured and the component emits an event content-updated, letting the parent know that something has happened.
  6. On Html you have v-on:content-updated. Since the parent is listening to the content-updated event, the parent method updateDescription will be called when the event is emited.

!!Couple Important Notes!!

  • By design, the component has 1 way binding, from parent to component. So when the description gets updated from Vue instance, the component's initValue property should be updated as well, automatically.
  • It would be nice if we can pass whatever the user types in tinymce editor back to the parent Vue instance but 2 ways bindings is not supposed. That's when you need to use $emit to fire up events and notify parents from components.
  • You don't have to define a function in parent and do v-on:content-updated="updateDescription". You can just directly update the data by doing v-on:content-updated="description = $event". The $event has the parameter you defined for the function inside the component - the newContent parameter.

Hope I explained things clearly. This whole thing took me 2 weeks to figure it out!!

like image 95
David Liang Avatar answered Oct 05 '22 05:10

David Liang


Here's a Tinymce component for Vue. http://jsbin.com/pucubol/edit?html,js,output

It's also good to know about v-model and custom input components: https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events

Vue.component('tinymce', {
    props: ['value'],
    template: `<div><textarea rows="10" v-bind:value="value"></textarea></div>`,
    methods: {
        updateValue: function (value) {
          console.log(value);
            this.$emit('input', value.trim());
        }
    },
    mounted: function(){
      var component = this;
      tinymce.init({ 
        target: this.$el.children[0],
        setup: function (editor) {
          editor.on('Change', function (e) {
            component.updateValue(editor.getContent());
          })
        }
      });
    }
});
<tinymce v-model="whatever"></tinymce>
like image 40
Pieter Avatar answered Oct 05 '22 03:10

Pieter