A small Vue.js app

Making copies of simple JavaScript objects

When I started writing an app in Vue.js, I was learning to work with key-value pairs stored in the Vue instance’s data option. Now that I am bundling groups of several such properties into objects stored in the tiddlers array, I find it would be more convenient to have them bundled from the start so that they can be referred to by a single name.

This appears to be a simple change, but it requires confronting how JavaScript deals with cloning objects (and working around it).

Goal

I will replace the data properties tempText and tempTags with a currentTiddler object that holds all the properties currently being edited in the UI.

Breakdown

  1. Replace tempText and tempTags with a data object called currentTiddler that will hold all current tiddler properties
  2. Adapt all the methods that used tempText or tempTags to use currentTiddler or its properties instead – this is where I have to be careful to make the right kind of copy when cloning currentTiddler

1. Add a currentTiddler object, to hold all current tiddler properties

With the idea in mind that a tiddler could have lots of properties beyond text and tags (and created date), I’ll corral all the working tiddler properties together in an object called currentTiddler. What used to be tempText and tempTags are now the text and tags properties of the currentTiddler object.

The data object of the Vue instance now looks like this:

data: {
  appTitle: 'Just changing something under the hood...',
  tiddlers: [],
  currentTiddler: {
    text: 'Texty text text. Semper ubi sub ubi',
    tags: ['onetag', 'twotag', 'threetag', 'four']
  }
},

The properties currentTiddler.text and currentTiddler.tags are initialized here. Any property that’s meant to be reactive in Vue needs to be initialized. I am not currently initializing created and utcOffset, because before the tiddler is “created” – i.e., pushed onto the tiddlers array – these properties don’t have values, and once they do exist they never change. Perhaps it would be best practice to declare them anyway, so anyone reading the code knows that they are properties that a tiddler can have. I’m leaving this for the moment.

2. Adapt all the methods that used tempText or tempTags to use currentTiddler.text or currentTiddler.tags instead

For the updateTempText(), addNewTag(), and removeTag() methods, this just means changing the name of each property. Making submitTiddler() method work will require more changes.

updateTempText():

I am taking the opportunity to rename this to the now-more-appropriate updateTiddlerText().

Before:

updateTempText(ev) {
  this.tempText = ev.target.innerText
},

After:

updateTiddlerText(ev) {
  this.currentTiddler.text = ev.target.innerText
},

This must be changed in the template, too:

<div id="text-tiddler" contenteditable="true" @blur="updateTiddlerText">{{currentTiddler.text}}</div>

addNewTag():

Another tweak while I am editing addNewTag(): now I replace the tags array with the Set() of its unique values rather than using an extra line to create a variable for that first:

Before:

addNewTag(ev) {
  this.tempTags.push(ev.target.textContent)
  var uniqueList = [...new Set(this.tempTags)]
  this.tempTags = uniqueList
  ev.target.textContent = ''
},

After:

addNewTag(ev) {
  this.currentTiddler.tags.push(ev.target.textContent)
  this.currentTiddler.tags = [...new Set(this.currentTiddler.tags)]
  ev.target.textContent = ''
},

removeTag():

Before:

removeTag(tag) {
  this.tempTags.splice(this.tempTags.indexOf(tag), 1)
},

After:

removeTag(tag) {
  this.currentTiddler.tags.splice(this.currentTiddler.tags.indexOf(tag), 1)
},

submitTiddler():

The submitTiddler() method needs a little bit of thought. Here is the old version:

submitTiddler() {
  var created = moment().utc().format('YYYYMMDDHHmmssSSS')
  var utcOffset = moment().utcOffset()
  var newTiddler = {
    created: created,
    utcOffset: utcOffset,
    text: this.tempText,
    tags: this.tempTags
  }
  this.tiddlers.push(newTiddler)
  this.tempText = ''
  this.tempTags = []
},

This creates the timestamp variables created and utcOffset, and makes up a new object called newTiddler with those, as well as tempTags and tempText, as properties. It pushes newTiddler onto the tiddlers array, then clears tempTags and tempText.

Here is the new version:

submitTiddler() {
  this.currentTiddler.created = moment().utc().format('YYYYMMDDHHmmssSSS') // In TW, times are stored in UTC
  this.currentTiddler.utcOffset = moment().utcOffset()
  this.tiddlers.push(this.currentTiddler)
  this.currentTiddler = JSON.parse(JSON.stringify(this.emptyTiddler))
},

The emptyTiddler object is defined in data, with the desired properties:

data: {
  //...
  emptyTiddler: {
        text: '',
        tags: []
      },
  //...
},

I no longer have to create the intermediary newTiddler object, because that’s now currentTiddler, which is already populated with text and tags properties. The timestamp and UTC offset are created as properties of currentTiddler, before it is pushed onto the tiddlers array.

It’s the very last line of this method that takes most of the thought. Clearing currentTiddler opens up a can of worms related to the way objects are cloned in JavaScript.

Non-primitive objects pushed onto arrays are references to the original object, not new copies. If I reach into currentTiddler and reset its properties, I also reset the properties of the object referenced in the tiddlers array.

So I want to point currentTiddler at a new object (leaving the one in the tiddlers array as the only reference to the previous object).

It’s tempting to set this.currentTiddler = this.emptyTiddler, but that makes a new reference to the object that emptyTiddler points to, and as soon as I start changing properties of currentTiddler, you guessed it: the properties of emptyTiddler change too.

I also cannot simply set currentTiddler to an empty object ({}), since I have the contents of some UI elements bound, using Vue’s mustache interpolation, to the values of currentTiddler.text and currentTiddler.tags. If I just vanish these properties, Vue can’t find them and doesn’t update the elements (and complains in the console).

Using this.currentTiddler = JSON.parse(JSON.stringify(this.emptyTiddler)) gets around this. It converts the emptyTiddler object into a string, then parses that into a JSON object and points currentTiddler to the new object. It looks hacky, but it works. It makes a fresh new object, and doesn’t carry over anything that JSON.stringify() doesn’t preserve (like functions). Perfect for my simple tiddler objects.

My app doesn’t seem to have changed. Good.