Nested BackboneJS Models

Nested BackboneJS Models

My relationship with Backbone.js is complicated. We have spent a dozen of hours together, full of entertaining and deep conversations (into late night hours). I was really impressed, excited by its simple and gentle perception of the world, its powerful abilities and benefits for one who decides to stick with it. I’ve seen colorful future, the future where we together, hand by hand, build beautiful, simple and reusable UI components, raise them in peace and happiness. In my dreams the components respect each other and cooperate, being attentive to other components’ events, listening carefully and responding appropriately and respectfully. That was a dream.

Nested Models

Here at Rollout.io we had a desprate need for a nested model — the data structures we work with have a strong “nested” orientation. There are many reasons for that, here are some of them that I think are reasonable:

  • Single network request is required to get all your data with a nested document-like structure, pulling subdocuments from an API would require multiple network requests, you’d also configure and implement API endpoints for each such request
  • Breaking a document to sub-documents (and assembling them back) is a tedious and time-consuming
  • Conceptual simplicity of nested model is obvious — while technically it may be more difficult to implement and use a nested model, it might be easier to understand the design of an application data layer while working with intuitive terms that are appropriately reflected as nested models

Feel the pain

Imagine you have a person schema in your MongoDB storage. Each person may have 0 or more phone numbers associated. Each phone number has a label, e.g. home, work, mobile and the actual number. A person document would be represented as:

var data = {
name: 'Bob',
lastName: 'Flop',
email: 'bob.flop@email.com',
phones: [
{
label: 'Home',
number: 101
},
{
label: 'Work',
number: 102
}
]
};
var personModel = new Backbone.Model(data);
_.keys(personModel.attributes)
> ["name", "lastname", "phones"]
personModel.get('phones[0].label')
> undefined
var phones = personModel.get('phones');
phones.map(function(phone) {
// do stuff with phone
});
var phones = personModel.get('phones');
var phoneModels = phones.map(function(phone) {
var phoneModel = new Backbone.Model(phone);
return phoneModel;
});
console.log(phoneModels);
> [Backbone.Model, Backbone.Model]
personModel.on('change', function (e) {
console.log('person has changed');
});
phoneModels[0].set('number',999);
// no change in person
  • Eventually we’d need to send the modified person model to backend, that will require to handle all the changes in phones sub-models and to send a unified single model to our backend servers.

The shine of nested model

Now, after we’ve tasted a bit of disadvantages when dealing with non-primitive models, imagine how wonderful would it be, if we could just easily access all nested attributes of the main person model and use native Backbone mechanics like events handling and validation.

var nestedPersonModel = new Backbone.NestedModel(data)
nestedPersonModel.get('phones[0].label')
> "Work"
nestedPersonModel.on('change:phones',
function () {
console.log ("Person has changed");
});
nestedPersonModel.set('phones[0].label', 'Office');
> Person has changed
// define validation - disallow duplicated phone labels
var NestedPersonClass = Backbone.NestedModel.extend({
validate: function (attrs, options) {
var uniqueLabels = _.uniq(_.pluck(attrs.phones, 'label'));
if (uniqueLabels.length !== attrs.phones.length) {
return 'Duplicated phone labels are not allowed';
}
}
});
var nestedPerson = new NestedPersonClass(data);
nestedPerson.set('phones[0].label', 'Work', {validate: true} )
> false

console.log(nestedPerson.validationError);
> "Duplicated phone labels are not allowed"

Messing with nested models

Despite all the advantages of nested models, they are still not perfect. The problem is that by using a nested data structures together with Backbone (and with Marionette extensions like CollectionView, Layout etc.), we are breaking the workflow and simplicity of Backbone views.

var SubCollection = Backbone.Collection.extend({
// sync data to the original nested model when needed
flushData: function(model, path) {
var arrayPlaceholder = [];
this.forEach(function(model) {
return arrayPlaceholder.push(model.toJSON());
});
model.set(path, arrayPlaceholder);
return model.save();
};

// bind the collection to the specific path of a nested model
bindToModel: function(model, path) {
var dataArray, item, i, len;
dataArray = model.get(path);
this.on('change', function() {
this.flushData(model, path);
});

this.on('remove', function() {
this.flushData(model, path);
});

for (i = 0, len = dataArray.length; i < len; i++) {
item = dataArray[i];
// create sub-models from the array elements of the nested model
this.add(item);
}
};
});

The sub-models utopia

So, what would be the ideal approach for working with complex data models? In order to fluently use all the goodies of Backbone-Marionette ecosystem, that’s designed to work with simple and flat models, without manually breaking the data structure into smaller not-connection chunks, it would be ideal to:

  1. Be able to create a collection of sub-models from array-like members of a nested model, having all the bindings listed above working correctly.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Andrew Goldis

Andrew Goldis

I like coffee, web development and simple code. https://agoldis.dev