Building an object model in Vue.js

Published
11 January 2022
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:

<template>
  // Some html here
</template>

<script>
  // Some javascript here
</script>

<style scoped>
  // Some CSS here
</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:

Author: string
Title: string
Year: integer
ISBN: string
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:

class Book {
  author = null
  title = null
  year = null
  isbn = null
  categories = []
}

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:

class Book {
  properties = {
    author: null,
    title: null,
    year: null,
    isbn: null,
    categories = []
  }
}

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:

class Book {
  #properties = {...}

  get author() {
    return this.#properties.author
  }

  set author(v) {
    this.#properties.author = v
  }

  // ...and so on...
}

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:

class Book {
  #properties = {...}
  #dirty = {}

  get author() {
    return this.#properties.author
  }

  set author(v) {
    // If this.#dirty doesn't already have the author in it, mark it as dirty
    if (!Object.keys(this.#dirty).includes("author")) {
      this.#dirty.author = this.#properties.author
    }

    this.#properties.author = v
  }
}

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:

class Book {
  // Mark the whole object as 'clean'. Useful for when we've synced our changes
  // with the database.
  clean() {
    this.#dirty = {}
  }

  // Revert to original properties. Useful for when a user makes a bunch of changes
  // and wants to discard them.
  revert() {
    Object.keys(this.#dirty).forEach(k => {
      this.#properties[k] = this.#dirty[k]
    })

    this.clean()
  }

  // Returns a list of modified properties
  get modifiedProperties() {
    let p = {}

    Object.keys(this.#dirty).forEach(k => p[k] = this.#properties[k])

    return p
  }
}

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:

class Book {
  static addProperty(property) {
    Object.defineProperty(
      this.prototype,
      property,
      {
        get: function() { return this.#properties[property] },
        set: function(v) {
          if (!Object.keys(this.#dirty).includes(property)) {
            this.#dirty[property] = this.#properties[property]
          }

          this.#properties[property] = v
        }
      }
    )

    return this
  }

  // Convenience function
  static addProperties(properties) {
    properties.forEach(p => this.addProperty(p))
    return this
  }
}

Book
  .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:

class Book {
  // We can't make this private as each instance of the `Book` class needs to read it.
  static _defaults = {}

  // This means that by default `defaultValue` will be set to `null`
   static addProperty(property, {defaultValue = null} = {}) {
    Object.defineProperty(
      this.prototype,
      property,
      {
        get: function() { return this.#properties[property] },
        set: function(v) {
          if (!Object.keys(this.#dirty).includes(property)) {
            this.#dirty[property] = this.#properties[property]
          }

          this.#properties[property] = v
        }
      }
    )

    // Add the default value to `_defaults`
    this._defaults[property] = defaultValue

    return this
  }

  // Convenience function
  static addProperties(properties, opts) {
    properties.forEach(p => this.addProperty(p, opts))
    return this
  }

  // And then we just need to set default values in the constructor!
  constructor() {
    Object.keys(this.constructor._defaults).forEach(k =>
      this[k] = this.constructor._defaults[k]
    )
  }
}

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:

class Book {
  // Object properties
  static _defaults = {}
  #properties = {}
  #dirty = {}

  // Constructor
  constructor() {
    Object.keys(this.constructor._defaults).forEach(k =>
      this[k] = this.constructor._defaults[k]
    )
  }

  // Adding properties
  static addProperty(property, {defaultValue = null} = {}) {
    Object.defineProperty(
      this.prototype,
      property,
      {
        get: function() { return this.#properties[property] },
        set: function(v) {
          if (!Object.keys(this.#dirty).includes(property)) {
            this.#dirty[property] = this.#properties[property]
          }

          this.#properties[property] = v
        }
      }
    )

    // Add the default value to `_defaults`
    this._defaults[property] = defaultValue

    return this
  }

  static addProperties(properties, opts) {
    properties.forEach(p => this.addProperty(p, opts))
    return this
  }

  // Dirty methods
  clean() {
    this.#dirty = {}
  }

  revert() {
    Object.keys(this.#dirty).forEach(k => {
      this.#properties[k] = this.#dirty[k]
    })

    this.clean()
  }

  get modifiedProperties() {
    let p = {}
    Object.keys(this.#dirty).forEach(k => p[k] = this.#properties[k])
    return p
  }
}

Book
  .addProperties(["author", "title", "year", "isbn"])
  .addProperty("categories", {defaultValue: []})

And here's some tests:

let b = new Book()

// Checking defaults
console.log(b.title) // => null
console.log(b.categories) // => []

// Setting title...
b.clean()
b.title = "The Hobbit"
console.log(b.title) // => "The Hobbit"
console.log(b.modifiedProperties) // => {title: "The Hobbit"}

// Reverting...
b.revert()
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. ↩︎