Hello,
today let's talk about Vue.Js and especially how to use it with editable contents (because it'll be used in Seshat). I won't go into how to install it and basic usage of Vue.JS, their guide is really good! For this sample, you only need a basic web page with jquery and Vue.js scripts.
The target
As Seshat is a tool to write a novel, I need to be able to add some format enhancers into the input zone and using an editable content is an easy way to have access to all the basic ones (and it's pretty well supported). However, Vue.JS does not support natively two-way data binding with editable contents.
Let's first start with a basic editable div:
<div id="editable" contenteditable="true"></div>
Nothing fancy, isn't it? ###Adding Vue.JS The object to store data is very simple:
var content = {
data: 'Hello <i>World</i>!'
}
*data* will contain the HTML string.
Ok, now we have container object and the HTML markup, let's add the magic. Let's declare our VueJs object:
var vueDiv = new Vue({
el: '#editable',
data: content
});
and update our div mark-up in order to display our initial content:
<div id="editable" contenteditable="true">
{{{* data}}}
</div>
Here, we have to use the {{{* }}}
notation in order to display content data on launch only, if we keep {{{ }}}
default notation, we'll end-up with an infinite loop later. Using {{{
instead of {{
is also mandatory to have inline HTML.
Updating javascript object
There is currently no automatic listener on changes applied inside the content so I'll use custom event mapping on several events: keyup, blur, paste, delete and focus. All of these events will be redirected to the same method changed inside my Vue object. Div declaration becomes:
<div id="editable" contenteditable="true" v-on:keyup="changed" v-on:blur="changed" v-on:paste="changed" v-on:delete="changed" v-on:focus="changed">
{{{* data}}}
</div>
Method changed is called each time an event of the previous list is triggered and receive the event as a parameter. This event's target is the editable div so by reading the html content you can update javascript object, uisng jquery, our Vue component becomes:
var vueDiv = new Vue({
el: '#editable',
data: content,
methods: {
changed: function(event) {
this.data = $(event.target).html().trim();
}
}
});
Now, when you write or change format of editable content, your javascript object will be updated. Because of this way to update javascript object not linked to v-model syntax, if we used `{{{ }}}`, when you changed div content the *changed* method is called, updates javascript object which is then refreshed in the div, starting the loop again (mainly because of trim() method). Current *changed* method only updates javascript object and is meant to be more complex in real life (like a push to server). In the current state, the method is called to often and may slow down your application. On VueJs, you can use *debounce* property with v-model but not here. So let's recreate it! First, we need a timeout object, linked to our Vue component:
var vueDiv = new Vue({
el: '#editable',
data: content,
timeoutID: null,
methods: {
...
}
});
You can add any common variable you want in your object, it'll be available inside methods using `$` prefix. *changed* method then becomes:
changed: function(event) {
clearTimeout(this.$timeoutID);
$that = this; // 'this' keyword will change of scope inside timeout function
this.$timeoutID = setTimeout(function() {
$that.data = $(event.target).html().trim();
}, 1000);
}
With this approach, even if changed method is called as before, the actual core of the update method is only called if no other call to changed is done during 1 second.
And that's it! Now, when you update editable div, your javascript object is updated (with debounce if you want to). Even when you change format using keyboard shortcut of when you copy/paste text.
Of course, this implementation could be transformed as a Vue component and with a variable timeout:
var editableContentVue = Vue.extend({
timeoutID: null,
timeoutDuration: 1000, // Default timeout duration
methods: {
changed: function(event) {
clearTimeout(this.$timeoutID);
$that = this;
this.$timeoutID = setTimeout(function() {
$that.data = $(event.target).html().trim();
}, this.$timeoutDuration);
}
}
});
var vueDiv = new editableContentVue({
el: '#editable',
data: content,
timeoutDuration: 2000 // Override specific to this instance
});
From javascript object to editable div
Current implementation does not allow a full two-way binding as if the javascript object is updated from outside the editable div, the content is not updated inside due to usage of {{{* }}}
notation. For my current usage I don't really need such flow as there won't be any possible update outside of editable zone but it could become an issue if I put in place multiple writters at once (like in GoogleDoc). The solution would be to put in place a lock inside the component to block the looping effect when the update is triggered by changed method.