A small Vue.js app
Text input with contenteditable
In the previous post, I set up one-way data binding, in the form of text interpolation, in my tiny Vue.js app. This made an <h2>
element display text stored in a data
property of the Vue instance.
The app now has three options populated in its definition: el
, which tells Vue which HTML element to mount to; data
, where I’ll be storing everything the app has to remember (this is a very simplistic approach that doesn’t scale, which I am sticking to for this basic learning project), and template
, which replaces the element indicated by el
with its own contents, with bindings to properties (and later methods) that give the app reactivity - and soon, interactivity.
Goal
The next step is for the app to take user input: text typed in by the user will be stored as the value of a key-value pair in the app’s data
object. This property represents the text of the note, or “tiddler”, as I like to call them (even though this isn’t TiddlyWiki) in my envisioned final app.
Just by setting an element’s contenteditable
attribute to "true"
, I can have it display whatever is typed into it in the browser. Although I’ll be using Vue to capture this input, contenteditable
is an HTML attribute, independent of Vue. My app layout already has two such elements: "text-tiddler"
and "newtag-tiddler"
.
In this post, I’ll get the contenteditable
element "text-tiddler"
functioning to store user input within the app. That means telling my Vue instance to store the innerText
of the element when the user has finished typing; I’ll use an event handler to achieve that.
Why use editable divs and not input
or textarea
elements? I think the answer may be that, usually, form input is a better solution, but because contenteditable
allows normal elements to be edited, it seems there either is, or has been in the past, more flexibility in styling than with forms. I’m exploring this approach for interest’s sake, as well as to keep this early step simple; forms have a lot of default behaviour, which, while potentially desirable, also means more things to think about at once.
I must include a disclaimer here: because contenteditable
generates HTML and
Breakdown
- Declare a
data
property in which to store the text that the user inputs. - Bind the contents of the chosen element (in this case the
<div>
withid="text-tiddler"
) to this property with thev-text
Vue directive. - Add a
method
to the app that updates this property with the edited text when invoked. - Attach an event listener to the
"text-tiddler"
<div>
, to invoke this method when the user clicks outside the<div>
or hits tab.
1. Add a data
property to hold text entered by the user
I’m declaring a property called tempText
in the data
object.
In general, if I want to use a property later, it should be declared, even if it’s empty. I’m giving tempText
a non-empty value, so there will be (editable) text showing in the text-tiddler
<div>
on load.
data: {
appTitle: 'A Vue.js app which allows the user to enter data via editable divs.',
tempText: 'Texty text text. Semper ubi sub ubi'
}
With the Vue DevTools extension on my browser, I can now look at my root component and see that it indeed has these data properties.
2. Bind the contents of the editable <div>
to the new property
Whenever the text-tiddler
element is refreshed, it should display the present value of tempText
(and it should be refreshed whenever tempText
's value changes). I’ll use the v-text
Vue directive to do this one-way data binding, as I did with the "app-title"
element in the previous post:
<div id="text-tiddler" v-text="tempText" contenteditable="true"></div>
Because I used a one-way data binding, typing into this <div>
does not change tempText
; but if tempText
is changed, the text inside the text-tiddler
<div>
updates to reflect that. Since there is, as yet, no other part of this app that might change data properties, the easiest way to test this is to edit the property in the Vue DevTools add-on, and watch what happens in the app.
3. Write a method to update tempText
to match the text in the editable <div>
Since the Vue instance doesn’t yet have any methods, I have to add the methods
option in its definition.
methods: {
updateTempText(ev) {
this.tempText = ev.target.innerText
}
}
The updateTempText(ev)
method is designed to be invoked by an event handler (which passes the event as the parameter ev
to the method). It replaces the value of tempText
with the innerText
of the event’s target
element (in this case the text-tiddler
<div>
).
I am using innerText
rather than textContent
, because the input could contain line breaks, and these are preserved (as \n
line feed characters) only with innerText
. Another option could be to use innerHTML
, but I don’t want to store <div>
tags with my data.
There are still complications with this.
One particular difficulty is that if I hit Enter
twice to enter a blank line, innerText
ends up with three consecutive line feeds (which is at least one too many for my liking). For now, I will mitigate this with CSS: because contenteditable
generates HTML, wrapping paragraphs in <div>
tags (in my current Firefox and Chrome installations, but not Safari), I can put a margin below each of these <div>
s. This way the user won’t be so tempted to enter multiple line breaks. A user could still wish to insert a blank line, and at this point my app is not going to handle that perfectly.
For reference, here is the CSS that pertains to this element:
#text-tiddler {
min-height: 5em;
max-height: 10em;
overflow-y: scroll;
white-space: pre-wrap;
}
#text-tiddler div {
margin-bottom: 0.8rem;
}
The second problem arises later, when I want to use the text-tiddler
element not only to enter text, but to display and edit it again. I’ll store the element’s innerText
as a data property, and when I display that again inside text-tiddler
, it won’t have the <div>
tags anymore. Thus, my CSS no longer provides a visual separation between paragraphs. I’m time-travelling here, but I’ll end(?) up writing a function using String.split() to reconstitute the <divs>
.
MDN’s Making content editable section explores controlling just what data is generated from input into a contenteditable
element.
On a related topic, using mustache interpolation in combination with linefeeds led to some strange results with duplicated content in the text-tiddler
element, whereas v-text
works fine.
Note that, per the Vue.js API, the this
context of a Vue instance’s method is bound to the instance. You’ll notice also that we don’t have to specify that it’s within the data
option.
4. Add an event listener/handler to invoke the update method
Finally, I need a listener that invokes updateTempText(ev)
when a given event occurs.
For now, I’ve chosen to update tempText
when the user tabs, or clicks, away from the text-tiddler
element. When that happens, the element emits a blur
(loss of focus) event, so I can use that event, and v-on:blur="updateTempText"
as the listener. It handles the event by invoking the method updateTempText(ev)
. I’ll use the shorthand @
for v-on:
, so that v-on:blur
becomes @blur
:
<div id="text-tiddler" v-text="tempText" contenteditable="true" @blur="updateTempText"></div>
Bonus diagnostic
I’ll tell the area below the text-tiddler
<div>
to display the current value of tempText
, to illustrate when the updates happen:
<div id="display-text">The current value of tempText is: "{{tempText}}"</div>
So we end up with the following:
(Note that the mustache interpolation doesn’t display line breaks.)
The app now has state that the user can change! The text displayed in the display-text
<div>
is now reactive to changes in the value of tempText
, which are triggered by the user changing the text in the text-tiddler
<div>
and then removing focus from it, e.g. by clicking elsewhere, or by hitting <tab>.
Note that the second editable <div>
, for tag input, still isn’t hooked into the app state in any way. You can type in it, and you can click the “Clear” and “Submit” buttons, but they have no effect.