8. Implementing a model

What's a model? Roughly, a model does a couple of things:

  • Data. A model contains data.
  • Events. A model emits change events when data is altered.
  • Persistence. A model can be stored persistently, identified uniquely and loaded from storage.

That's about it, there might be some additional niceties, like default values for the data.

Defining a more useful data storage object (Model)

function Model(attr) {
this.reset();
attr && this.set(attr);
};
Model.prototype.reset = function() {
this._data = {};
this.length = 0;
this.emit('reset');
};

Model.reset()

_data: The underlying data structure is a object. To keep the values stored in the object from conflicting with property names, let's store the data in the _data property

Store length: We'll also keep a simple length property for quick access to the number of elements stored in the Model.

Model.prototype.get = function(key) {
return this._data[key];
};

Model.get(key)

This space intentionally left blank.

Model.prototype.set = function(key, value) {
var self = this;
if(arguments.length == 1 && key === Object(key)) {
Object.keys(attr).forEach(function(key) {
self.set(key, attr[key]);
});
return;
}
if(!this._data.hasOwnProperty(key)) {
this.length++;
}
this._data[key] = (typeof value == 'undefined' ?
true : value);
};

Model.set(key, value)

Setting multiple values: if only a single argument Model.set({ foo: 'bar'}) is passed, then call Model.set() for each pair in the first argument. This makes it easier to initialize the object by passing a hash.

Note that calling Model.set(key) is the same thing as calling Model.set(key, true).

What about ES5 getters and setters? Meh, I say.

Setting a single value: If the value is undefined, set to true. This is needed to be able to store null and false.

Model.prototype.has = function(key) {
return this._data.hasOwnProperty(key);
};
Model.prototype.remove = function(key) {
this._data.hasOwnProperty(key) && this.length--;
delete this._data[key];
};
module.exports = Model;

Model.has(key), Model.remove(key)

Model.has(key): we need to use hasOwnProperty to support false and null.

Model.remove(key): If the key was set and removed, then decrement .length.

That's it! Export the module.

Change events

Model accessors (get/set) exist because we want to be able to intercept changes to the model data, and emit change events. Other parts of the app -- mainly views -- can then listen for those events and get an idea of what changed and what the previous value was. For example, we can respond to these:

  • a set() for a value that is used elsewhere (to notify others of an update / to mark model as changed)
  • a remove() for a value that is used elsewhere

We will want to allow people to write model.on('change', function() { .. }) to add listeners that are called to notify about changes. We'll use an EventEmitter for that.

If you're not familiar with EventEmitters, they are just a standard interface for emitting (triggering) and binding callbacks to events (I've written more about them in my other book.)

var util = require('util'),
events = require('events');
function Model(attr) {
// ...
};
util.inherits(Model, events.EventEmitter);
Model.prototype.set = function(key, value) {
var self = this, oldValue;
// ...
oldValue = this.get(key);
this.emit('change', key, value, oldValue, this);
// ...
};
Model.prototype.remove = function(key) {
this.emit('change', key, undefined, this.get(key), this);
// ...
};

The model extends events.EventEmitter using Node's util.inherits() in order to support the following API:

  • on(event, listener)
  • once(event, listener)
  • emit(event, [arg1], [...])
  • removeListener(event, listener)
  • removeAllListeners(event)

For in-browser compatibility, we can use one of the many API-compatible implementations of Node's EventEmitter. For instance, I wrote one a while back (mixu/miniee).

When a value is set(), emit('change', key, newValue, oldValue).

This causes any listeners added via on()/once() to be triggered.

When a value is removed(), emit('change', key, null, oldValue).

Using the Model class

So, how can we use this model class? Here is a simple example of how to define a model:

function Photo(attr) {
  Model.prototype.apply(this, attr);
}

Photo.prototype = new Model();

module.exports = Photo;

Creating a new instance and attaching a change event callback:

var badger = new Photo({ src: 'badger.jpg' });
badger.on('change', function(key, value, oldValue) {
  console.log(key + ' changed from', oldValue, 'to', value);
});

Defining default values:

function Photo(attr) {
  attr.src || (attr.src = 'default.jpg');
  Model.prototype.apply(this, attr);
}

Since the constructor is just a normal ES3 constructor, the model code doesn't depend on any particular framework. You could use it in any other code without having to worry about compatibility. For example, I am planning on reusing the model code when I do a rewrite of my window manager.

Differences with Backbone.js

I recommend that you read through Backbone's model implementation next. It is an example of a more production-ready model, and has several additional features:

  • Each instance has a unique cid (client id) assigned to it.
  • You can choose to silence change events by passing an additional parameter.
  • Changed values are accessible as the changed property of the model, in addition to being accessible as events; there are also many other convenient methods such as changedAttributes and previousAttributes.
  • There is support for HTML-escaping values and for a validate() function.
  • .reset() is called .clear() and .remove() is .unset()
  • Data source and data store methods (Model.save() and Model.destroy()) are implemented on the model, whereas I implement them in separate objects (first and last chapter of this section).
comments powered by Disqus