Nested BackboneJS Models

Written by: Andrew Goldis

  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. After a while I’ve started to pay attention that not everything is perfect. Backbone was demanding, gelous for details. It required all my attention while we were rendering views together, we had fun preventing memory leaks, but it was difficult as well, as both of us were blaming each other for creating them. I was told once that I do not really understand it. Slowly, my excitement started to decline, I’ve discovered myself starting to look at other front-end frameworks, they were sexier, younger, more attractive. However, after many hours and lines of code that we’ve spent together, it was hard to break up just like that. I had obligations as well - I’ve promised to take care of the views and models we’ve created together, respect all the effort and investment we’ve made so far. We’ve agreed to make some changes, used Marionette to make Backbone more attractive, attached some plugins for cosmetics and performance. Things look much better since then. We manage to be together, despite all the difficulties, not always agreeing and understanding each other, but we do have fun; the secret is to find a common language and be respectful to each other’s demands.

Nested Models

Here at CloudBees Feature Management.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:

  • The document storage concept of MongoDB implies using a similar concept at front-end side - i.e. you get a single document that might have a nested subdocuments

  • 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

There’s quite a lot of discussion about the concept of nested, or deep model, including the comment of Backbone.js creator, Jeremy Ashkenas on GitHub and BackboneJS official FAQ section. The arguments of not using nested model sound reasonable and it is up to you to decide whether to use the concept or stick with the original interpretation of flat, simple models as proposed by respective developers. To showcase the problem, here’s a small example that highlights the issues I’ve mentioned

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
       }
   ]
};

The document presentation as a Backbone model would have the following keys of its attributes:

var personModel = new Backbone.Model(data);
_.keys(personModel.attributes)
> ["name", "lastname", "phones"]

You cannot get access to phone object details directly using Backbone.Model.get :

personModel.get('phones[0].label')
> undefined

In order to access the sub-documents of phone , it’s required to iterate over the phones  array:

var phones = personModel.get('phones');
phones.map(function(phone) {
  // do stuff with phone
});

The instance of phone  object is not a Backbone model - you cannot apply the common Backbone patterns (like 'get', 'set', 'validation' , there’s no events propagation). In order to use the object, we need to backbonify it, creating a new generic Model and wrap phone  object in it:

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]

However, the newly created phone models are not connected in any matter to the original person model. That means that:

  1. No events propagation across the model, changing an attribute on phone model doesn’t trigger change event on person

personModel.on('change', function (e) {
  console.log('person has changed');
});
phoneModels[0].set('number',999);
// no change in person
  1. No validation happens for person model when phone  model is changed, i.e. you need to implement distinct validation logic for phone  and for person  model in order to get the validation process happen as soon as some property of phone model changes.

  2. 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. That’s what nested model allow us to do. The main advantage is that all nested attributes are accessible directly, using native JS syntax. There’re different implementations of nested models available, here are some of them:

There’re also several projects that implement the concept of “complex” model by applying a “relational” approach, i.e. predefining relations between different models and handling inter-relationships accordingly:

We’ll skip the relational (or related) models and will focus on nested models implementation - the usage is very simple, extending your model from a generic Nested Model would give you effortless access to all its attributes, triggering events properly and handling validation process fluently:

  1. Get access to nested data with no effort, using JS syntax:

var nestedPersonModel = new Backbone.NestedModel(data)
nestedPersonModel.get('phones[0].label')
> "Work"
  1. Events are handled properly:

nestedPersonModel.on('change:phones',
  function () {
    console.log ("Person has changed");
});
nestedPersonModel.set('phones[0].label', 'Office');
> Person has changed
  1. Validation works as expected:

// 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"

As we’ve seen, most of the problems we’ve been suffered when using the original, flat models, are solved. Now we can work with the unified model that has identical presentation in storage and front-end.

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. The Views (and Collection / Layout views) are designed to work with simple, flat models. Having a unified but complicated nested model and passing it to views requires specifying what part of the ‘big’ model is accessible for the specific view. We've been utilizing this technique, passing a nested model between Layout and its sub-views, after reaching a certain level of complexity, we’ve started to get messed with defining paths for ItemViews that get the whole nested model to properly define data chunk that the view should have access to:

The complexity of serving the correct model for ItemViews is in the scope of the Layout View, and often it is not trivial to change the current sub-item (technically, changing the index i or j). The implementation of the ItemViews and Layout were not intuitive and hard-to-follow, as every accesschange in the nested model required recalculating the paths (index values) and re-rendering the dependant part of associated Views. Moreover, the bigger pain we’ve felt was the lack of ability to create a native collections that may be used by CollectionViews properly. As a workaround we’ve started to create a ‘dummy’ collections that are ‘bound’ to the original nested model and serve as a proxy for convenient using in collection-based views.

The workaround we’ve used allowed us to overcome the lack of ability to natively create collections from embedded nested elements. However, the solution we’ve used was not generic enough and was tailored only for specific needs; for example, the binding between proxy collection and the nested collection is one-directional, i.e. changes in the nested model are not being reflected in the proxy collection elements (we had no need to propagate changes in other direction). The simplified version of the ‘proxy’ collection looks like:

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 new sub-model from any path of the ‘big’, nested model. The newly created sub-model should behave like a distinct Backbone model, but is is also has to be tightly bound to its ‘parent’ in terms of data integrity, events propagation, validation etc.

  2. 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.

The approach that’s presented in the ‘dummy’ proxy collection is quick and dirty trial to implement this solution with very limited functionality, the generic solution (I wish I had) should have a bidirectional binding and well-defined events propagation logic. Having such an powerful feature would significantly enhance developers’ ability to deal with complex data structures, creating sub-models and sub-collection on demand, and using them natively with Backbone/Marionette Views and Layouts.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.