BAREBONE Models

Posts in this series:

Guest Post by the Awesome Kalev Roomann-Kurrik

The first component of the BAREBONE.js framework that we will dive into is the Model class.

Models in both Backbone.js and our BAREBONE.js framework are used to represent basic pieces of data, and the logic used to manipulate that data. For instance, in our RSVP application each guest model will be represented as an instance of a Guest class that we define.

Let’s take a quick look at what a model might look like. For our RSVP application we want to create a Guest class that we will use to construct our guest model objects. We’ll cover the BAREBONE.Model.extend method in a later post, for now just know that it returns a constructor function.

var Guest = BAREBONE.Model.extend({
defaults: {
attending: false
}
});

Now we can create a new guest model. Since Kalev is always the life of any party, let’s put him on the guest list!

// instantiate a new Guest object
var kalev = new Guest({
name: 'Kalev Roomann-Kurrik'
});

Here’s what our model object looks like:

kalev = {
attributes: {
name: 'Kalev Roomann-Kurrik',
attending: false
}
}

But there’s more to the ‘kalev’ model than just what you see here… There are a whole bunch of useful methods on the model’s prototype that have logic to manipulate the model’s data. So why do we use Backbone models rather than POJOs (plain old JavaScript objects)?

We interact with the data on our models by using the .set method that lives in the model’s prototype chain.

kalev.set('attending', true);

Now our ‘kalev’ model looks like this:

kalev = {
attributes: {
name: 'Kalev Roomann-Kurrik',
attending: true
}
}

Why do we do this instead of just directly mutating the ‘attending’ property? Like so…

// DON'T DO THIS!!
kalev.attributes.attending = true;

Models are more powerful than using regular JavaScript objects to represent our data because they have built-in event triggers which can be used to notify other parts of our application when the data in our Model changes. When we use the .set method a ‘change’ event gets triggered on the ‘kalev’ model.

kalev.set('attending', true);  //--> 'change' event triggered!!!

Now other parts of our application can hear the ‘change’ event and perform some action, such as re-rendering a View. We will briefly touch on event triggers when going through our Model class code, but the BAREBONE Events post has a more detailed explanation of the events system.

So now let’s dive into the BAREBONE.js source code to get a better sense of what’s going on with our models.

Here’s some skeleton code to get a sense of what’s coming:

/* BAREBONE.js code */
// Model constructor function
var Model = BAREBONE.Model = function(attributes) {
// set attributes and default properties on model
// invoke initialize method to execute initial logic, if any
};
// Add methods to the constructor's prototype object
_.extend(Model.prototype, Events, {
initialize: function() {},
get: function(attr) {...},
set: function(key, val) {...}
});

Note the methods added to the model constructor’s prototype. These are important and we’ll be looking at them more closely shortly. First though, let’s look at the constructor function…

BAREBONE.Model Constructor Function

Let’s start by defining a constructor function for our Model class:

/* BAREBONE.js code */
// Create a new model with the specified attributes
var Model = BAREBONE.Model = function(attributes) {
  // Attach `attributes` (initial values) and `defaults` to model.
var attrs = attributes || {};
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
this.attributes = {};
this.set(attrs);
  // Invoke `initialize`. First parameter to initialize will be model's
// `attributes` object.
this.initialize.apply(this, arguments);
};

Here we are using JavaScript’s pseudoclassical instantiation pattern to define our Model class (notice the ‘this’ keyword in the body). The Model constructor function is used to create new instances of our Model class, and the Model’s methods are put on its prototype.

Let’s take a closer look at the code:

  • Create ‘attrs’ object to store all model attributes
var attrs = attributes || {};

The attributes parameter contains any initial values that we can to attach to our Model object when we instantiate it. For example, for the ‘kalev’ model we had an attributes object with a ‘name’ property.

var kalev = new Guest({
name: 'Kalev Roomann-Kurrik'
});
// attributes = {name: 'Kalev Roomann-Kurrik'}

If no attributes are passed in on construction of our Model object then we set attributes to an empty object, so that the next line of code doesn’t break.

  • Now let’s add any defaults
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));

Remember how when we made our Guest Class we set the default ‘attending’ property value to false? Here’s where that gets added. These default properties are defined when we extended the base Model class using BAREBONE.Model.extend (we’ll cover this in a later post).

The order of the arguments to the _.defaults method ensures that any passed in attributes will not be overwritten by any default properties which share the same key. _.results is an Underscore.js method which returns the value for the property name provided.

  • Create the ‘attributes’ property and initialize it to an empty object. Then use the set method on the prototype chain to populate that empty object with the properties on the ‘attrs’ object.
this.attributes = {};
this.set(attrs);

The Model class’s set method is used to add the combined attributes properties and default properties to the Model object. We’ll take a closer look at this method shortly.

  • The last thing that we do in the Model’s constructor function is call the Model class’s initialize method with any passed in attributes.
this.initialize.apply(this, arguments);

The initialize method does not contain any logic by default, but if the Model class is extended (again, we’ll cover this in a later post) then the method can be overwritten with any code that should be run each time that a new instance of the class is created.

For example, we could have added an initialize method when we created our Guest Class:

var Guest = BAREBONE.Model.extend({
defaults: {
attending: false
},
initialize: function(guest) {
console.log('created new guest model with name', guest.name);
}

});

So when we instantiate a new Guest object, we will see something print in our console:

var backboneCreator = new Guest({name: 'Jeremy Ashkenas'});
// console says: 'created new guest model with name Jeremy Ashkenas'

What kind of BAREBONE.js BONEanza party would it be without Jeremy Ashkenas, the author of Backbone.js?!?

After defining the constructor function for the Model class, we next attach all inheritable methods to the Model prototype as well as the Events module

Model Class Prototype Methods

/* BAREBONE.js code */
// Attach all inheritable methods to the Model prototype.
// Add the Events module so that the model can bind and trigger events.
// Add model-specific methods.
_.extend(Model.prototype, Events, {
  // Override with initialization logic. Empty function by default.
initialize: function() {},
  // Get the value of an attribute.
get: function(attr) {
return this.attributes[attr];
},
  // Set or update the value of an attribute.
set: function(key, val) {
    var attrs;
    if (key == null) return this;
    // Input may be object or string. Create `attrs` object.
if (typeof key === 'object') {
attrs = key;
} else {
(attrs = {})[key] = val;
}
    // Add or change attribute in `this.attributes` object.
for (var attr in attrs) {
this.attributes[attr] = attrs[attr];
}
    // Trigger change event on object.
this.trigger('change', this);
return this;
}
});

We use the _.extends method to add on the Events module, so that our Models can trigger and listen to events, and the initialize, get, and set methods.

  • As stated previously, the initialize method is empty by default, but can be overwritten if the base Model class is extended.
Model.prototype.initialize = function() {}
  • The get method is used to return an attribute’s value given its name.
Model.prototype.get = function(attr) {
return this.attributes[attr];
}
  • The set method is used to add or update attributes on our Model object based on the passed in key and value arguments.
Model.prototype.set = function(key, val) {
  var attrs;
  if (key == null) return this;
  // Input may be object or string. Create `attrs` object.
if (typeof key === 'object') {
attrs = key;
} else {
(attrs = {})[key] = val;
}
  // Add or change attribute in `this.attributes` object.
for (var attr in attrs) {
this.attributes[attr] = attrs[attr];
}
  // Trigger change event on object.
this.trigger('change', this);
return this;
};

If the key argument is an object then the method creates a reference (‘attrs’) to that object. Otherwise, if the key is not an object, then a property is created on an empty object using the passed in key and value, and a reference (‘attrs’) to that object is created.

The ‘attrs’ object which now contains the passed in attribute(s) is iterated over, and the Model object’s attributes property is updated with the new attribute(s). This has the affect of adding new attributes if they don’t yet exist on our Model object, or updating attributes if they already exist.

Finally, we use the trigger method, which we have access to because we extended our Model class with our Events module, to trigger an event notifying any listeners that the attributes in our Model object have changed. Here we have a general event for any changes to our Model object’s attributes in order to keep our code simpler, but the actual Backbone.js library allows for listening to changes to specific attributes.

Mix in Underscore Methods

The final action that we perform on our Model class is to mix in Underscore methods into our Model class. These methods can be used as helper methods on our Models, and we use the Underscore implementations to avoid having to rewrite our own implementations.

/* BAREBONE.js code */
// Underscore methods to implement on our Model.
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit', 'chain', 'isEmpty'];
// Mix in Underscore methods as a proxy to the `model.attributes`.
_.each(modelMethods, function(method) {
Model.prototype[method] = function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this.attributes);
return _[method].apply(_, args);
};
});

This code sets up all of the Underscore methods in the modelMethods array as properties on the Model prototype, so that they act like built-in methods.

That’s all of the code that we need to define our BAREBONE.js Models!

RSVP Sample Application

Now, let’s look at all of our RSVP application code to see how we would define our own Model by extending this base Model class, and create instances of it:

// Create `Guest` Model.
// Set default `attending` property to false.
// This will be toggled on click of the attendee's `name`.
var Guest = BAREBONE.Model.extend({
defaults: {
attending: false
},
initialize: function(guest) {
console.log('created new guest model with name', guest.name);
}
});
// Instantiate instance of `Guest` MODEL.
// Who invited these two nerds...?
var katrina = new Guest({
name: 'Katrina Uychaco'
});
var kalev = new Guest({
name: 'Kalev Roomann-Kurrik'
});

Let’s review what this code is doing:

Extend Base Model Class

We create our own Guest Model class by extending the base BAREBONE.js Model class that we defined our own library. We will cover the implementation of the extend method in a later post in this series, but for now it’s enough to understand that we are using the extend method to setup a default attribute for all instances of our Guest Model.

var Guest = BAREBONE.Model.extend({
defaults: {
attending: false
},
initialize: function(guest) {
console.log('created new guest model with name', guest.name);
}
});

All Guest instances will have an attending attribute with an initial value of false. Remember that with how we defined our Model class constructor function the defaults attribute will be added on to our Model instance along with any instance-specific attributes which are passed in at instantiation (such as an initialize method).

Create Instances of Model Class

We create instances of our Guest Model class using the new keyword, and pass in an object with the properties that represent the attributes that we want on our Model instance.

var katrina = new Guest({
name: 'Katrina Uychaco'
});
var kalev = new Guest({
name: 'Kalev Roomann-Kurrik'
});

For our guests, each one has a name property, which we will reference when displaying our guests in our BAREBONE.js Views.

That completes our dive into the BAREBONE.js Model class!

Next: BAREBONE.js Collections!

Next we will explore the BAREBONE.js Collection class, and how it lets us conveniently work with collections of data!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.