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 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 Note that calling 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
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 This causes any listeners added via on()/once() to be triggered. When a value is |
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).