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

  1. Declare a data property in which to store the text that the user inputs.
  2. Bind the contents of the chosen element (in this case the <div> with id="text-tiddler") to this property with the v-text Vue directive.
  3. Add a method to the app that updates this property with the edited text when invoked.
  4. 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.