Building an object model in Vue.js

Published
2021-12-11
Tagged

Over the past six months or so I’ve been learning Vue.js. It’s a real interesting trip - back when I started getting interested in programming Ruby on Rails was the New Hotnessā„¢, and I spent a lot of time mucking about with MVC-styled apps and WidgetControllers and the like. Now, some fifteen years later, everything is client-side and javascript!1

Anyway, if there’s one thing harder than getting someone to latch onto an idea, it’s getting someone to let go of it. That’s why, as soon as I got past the “how does it work?” phase of learning Vue, I started building things using ye olde CRUD patterns and object models that look suspiciously like rails’ ActiveRecord.

A couple of projects in, I’m starting to get some patterns down. It’s an interesting problem - how do we represent database objects and the conceptual objects they represent, track their modification by users, and save them back to the database? - and I thought it would be good to show off some tricks here.

In this post (hopefully the first of a few), I’ll go into the basics of an object model that can track properties and how they change. This will be step one of about five (I guess?) in the object model, and it’ll build a strong foundation for our more advanced tricks.

Context: Putting one of the Ms in MVVM

Vue is nominally designed to fit the MVVM or Model-View-ViewModel pattern. This means that if we’re designing our projects properly we should find that we end up with:

  • An entity which simulates the conceptual object we’re manipulating, including talking with the database (the model)
  • An entity which displays the object to the user through the web page (the view)
  • An entity which acts as an interpreter between the two of these, computing any derived variables and tracking anything else that falls between these two (the view model).

Once you start poking around with Vue, you’ll start seeing components rendered as:

1
<template>
2
  // Some html here
3
</template>
4
5
<script>
6
  // Some javascript here
7
</script>
8
9
<style scoped>
10
  // Some CSS here
11
</style>

Your <template> is the actual HTML rendered by the component. It’s our view in thise case. The <script> section allows us to define things like data, properties, computed values, and so on. It could conceivably act as our model, but I feel it’s better as a viewmodel. Instead, we can create a separate model object, and recycle that across many components.

In this post (which will hopefully be the first of a few) I’ll look at how we can build a model object in javascript that simulates Rail’s ActiveRecord (at least in the ways where this is helpful to us).

What does our model need to do?

Obviously every framework is arbitrary, but to my mind here’s what our model needs to do:

  • Load our object from our database
  • Syncronise any changes with the database
  • Allow us to create new objects
  • Perform validation on object properties

I’ll be using Fauna as my database of choice for this post - you should be able to swap this (and the relevant code) out for your own preferred database. For this post we’re going to try building a database of books, with the following properties:

1
Author: string
2
Title: string
3
Year: integer
4
ISBN: string
5
Categories: array

Actually building in properties

First of all, how should we represent model properties (that is, those properties we’ll be storing in the database) in our model? It’s tempting to just make them javascript properties:

1
class Book {
2
  author = null
3
  title = null
4
  year = null
5
  isbn = null
6
  categories = []
7
}

However! This means that model properties get all mixed up with the rest of the logic we’re going to build. For this reason, I’d advocate storing all the model properties in a dictionary:

1
class Book {
2
  properties = {
3
    author: null,
4
    title: null,
5
    year: null,
6
    isbn: null,
7
    categories = []
8
  }
9
}

How do we access these properties then? Do we always have to call book.properties.author? We could do this, or we could set up our own custom getter and setter methods for each property. In fact, this lets us make the properties dictionary private, forcing other classes to interact through our defined getters and setters:

1
class Book {
2
  #properties = {...}
3
4
  get author() {
5
    return this.#properties.author
6
  }
7
8
  set author(v) {
9
    this.#properties.author = v
10
  }
11
12
  // ...and so on...
13
}

A lot of the time we’ll want to trigger events to occur when we set object properties. For example, it’s usually useful to track which properties have changed since we loaded the object (we’ll call these “dirty” properties). Now we have custom getters and setters, this is nice and easy:

1
class Book {
2
  #properties = {...}
3
  #dirty = {}
4
5
  get author() {
6
    return this.#properties.author
7
  }
8
9
  set author(v) {
10
    // If this.#dirty doesn't already have the author in it, mark it as dirty
11
    if (!Object.keys(this.#dirty).includes("author")) {
12
      this.#dirty.author = this.#properties.author
13
    }
14
15
    this.#properties.author = v
16
  }
17
}

This means that, the first time we change the model’s author property, we’ll mark the author property as dirty, and also store the original value in the #dirty property. What good is that? Well, here’s some additional methods we can set up:

1
class Book {
2
  // Mark the whole object as 'clean'. Useful for when we've synced our changes
3
  // with the database.
4
  clean() {
5
    this.#dirty = {}
6
  }
7
8
  // Revert to original properties. Useful for when a user makes a bunch of changes
9
  // and wants to discard them.
10
  revert() {
11
    Object.keys(this.#dirty).forEach(k => {
12
      this.#properties[k] = this.#dirty[k]
13
    })
14
15
    this.clean()
16
  }
17
18
  // Returns a list of modified properties
19
  get modifiedProperties() {
20
    let p = {}
21
22
    Object.keys(this.#dirty).forEach(k => p[k] = this.#properties[k])
23
24
    return p
25
  }
26
}

As you may guess, these auxiliary functions are going to come in handy later on.

Let’s DRY things up

If you’re writing a model with a bunch of properties, it can get boring to write getter and setter methods for each model property. For this reason, we can write a quick static generator function which sets them up for us:

1
class Book {
2
  static addProperty(property) {
3
    Object.defineProperty(
4
      this.prototype,
5
      property,
6
      {
7
        get: function() { return this.#properties[property] },
8
        set: function(v) {
9
          if (!Object.keys(this.#dirty).includes(property)) {
10
            this.#dirty[property] = this.#properties[property]
11
          }
12
13
          this.#properties[property] = v
14
        }
15
      }
16
    )
17
18
    return this
19
  }
20
21
  // Convenience function
22
  static addProperties(properties) {
23
    properties.forEach(p => this.addProperty(p))
24
    return this
25
  }
26
}
27
28
Book
29
  .addProperties(["author", "title", "year", "isbn", "categories"])

Now we’ve got all the getters and setters we could want, in record time! Now, however, we’re defining our properties in two places - once when we create the #properties private property, and once when we call the .addProperties() method. Wouldn’t it be nice if we just did it once? Of course, we need to ensure that we keep our lovely default values (for example, we want categories to be an array by default). We’re going to add a new argument to the addProperties() method, so we can (if we want) provide a default value. We’re then going to store this on a static property that we reference during object creation:

1
class Book {
2
  // We can't make this private as each instance of the `Book` class needs to read it.
3
  static _defaults = {}
4
5
  // This means that by default `defaultValue` will be set to `null`
6
   static addProperty(property, {defaultValue = null} = {}) {
7
    Object.defineProperty(
8
      this.prototype,
9
      property,
10
      {
11
        get: function() { return this.#properties[property] },
12
        set: function(v) {
13
          if (!Object.keys(this.#dirty).includes(property)) {
14
            this.#dirty[property] = this.#properties[property]
15
          }
16
17
          this.#properties[property] = v
18
        }
19
      }
20
    )
21
22
    // Add the default value to `_defaults`
23
    this._defaults[property] = defaultValue
24
25
    return this
26
  }
27
28
  // Convenience function
29
  static addProperties(properties, opts) {
30
    properties.forEach(p => this.addProperty(p, opts))
31
    return this
32
  }
33
34
  // And then we just need to set default values in the constructor!
35
  constructor() {
36
    Object.keys(this.constructor._defaults).forEach(k =>
37
      this[k] = this.constructor._defaults[k]
38
    )
39
  }
40
}

Now we’re really motoring along! We’ve got a class that will accept a number of predefined model properties, will track which have changed, and will also provide defaults. Let’s check it out in practice. Our model now looks like this:

1
class Book {
2
  // Object properties
3
  static _defaults = {}
4
  #properties = {}
5
  #dirty = {}
6
7
  // Constructor
8
  constructor() {
9
    Object.keys(this.constructor._defaults).forEach(k =>
10
      this[k] = this.constructor._defaults[k]
11
    )
12
  }
13
14
  // Adding properties
15
  static addProperty(property, {defaultValue = null} = {}) {
16
    Object.defineProperty(
17
      this.prototype,
18
      property,
19
      {
20
        get: function() { return this.#properties[property] },
21
        set: function(v) {
22
          if (!Object.keys(this.#dirty).includes(property)) {
23
            this.#dirty[property] = this.#properties[property]
24
          }
25
26
          this.#properties[property] = v
27
        }
28
      }
29
    )
30
31
    // Add the default value to `_defaults`
32
    this._defaults[property] = defaultValue
33
34
    return this
35
  }
36
37
  static addProperties(properties, opts) {
38
    properties.forEach(p => this.addProperty(p, opts))
39
    return this
40
  }
41
42
  // Dirty methods
43
  clean() {
44
    this.#dirty = {}
45
  }
46
47
  revert() {
48
    Object.keys(this.#dirty).forEach(k => {
49
      this.#properties[k] = this.#dirty[k]
50
    })
51
52
    this.clean()
53
  }
54
55
  get modifiedProperties() {
56
    let p = {}
57
    Object.keys(this.#dirty).forEach(k => p[k] = this.#properties[k])
58
    return p
59
  }
60
}
61
62
Book
63
  .addProperties(["author", "title", "year", "isbn"])
64
  .addProperty("categories", {defaultValue: []})

And here’s some tests:

1
let b = new Book()
2
3
// Checking defaults
4
console.log(b.title) // => null
5
console.log(b.categories) // => []
6
7
// Setting title...
8
b.clean()
9
b.title = "The Hobbit"
10
console.log(b.title) // => "The Hobbit"
11
console.log(b.modifiedProperties) // => {title: "The Hobbit"}
12
13
// Reverting...
14
b.revert()
15
console.log(b.title) // "The Hobbit"

So there we have it! A pretty robust model property framework. It’s not hooked up to anything yet, but these tools will give us plenty to work with in future posts.


  1. To give you some context, back when I was poking around with Rails, we were still debating whether or not jQuery was anything to write home about. Since then, we decided jQuery was the best thing since sliced bread, and then javascript caught up, and now I feel the consensus is that jQuery has done its dash and let’s just use core JS.