Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cast/initialize submodels of a Backbone Model

I think I have a pretty simple problem that is just pretty difficult to word and therefore hard to find a solution for. Setup:

  • PathCollection is a Backbone.Collection of Paths
  • Path is a Backbone.Model which contains NodeCollection (which is a Backbone.Collection) and EdgeCollection (which is a Backbone.Collection).

When I fetch PathCollection

paths = new PathCollection()
paths.fetch()

obviously, Paths get instantiated. However, I'm missing the spot where I can allow a Path to instantiate its submodels from the attribute hashes. I can't really use parse, right? Basically im looking for the entry point for a model when its instantiated and set with attributes. I feel like there must be some convention for it.

like image 831
nambrot Avatar asked Sep 10 '12 10:09

nambrot


People also ask

What is a model backbone?

Model. Models are the heart of any JavaScript application, containing the interactive data as well as a large part of the logic surrounding it: conversions, validations, computed properties, and access control. You extend Backbone.

What is Backbone in programming?

Backbone. js is a model view controller (MVC) Web application framework that provides structure to JavaScript-heavy applications. This is done by supplying models with custom events and key-value binding, views using declarative event handling and collections with a rich application programming interface (API).

Is Backbone JS still relevant?

Backbone. Backbone has been around for a long time, but it's still under steady and regular development. It's a good choice if you want a flexible JavaScript framework with a simple model for representing data and getting it into views.


1 Answers

So I've written a couple of answers regarding using parse() and set() to instantiate and populate sub-models and sub-collections (nested data). However, I haven't seen a really comprehensive answer that consolidates some of the many practices I've seen. I tend to ramble a bit when I write a lot so I might digress a little but this might be useful for people going through similar kinds of issues.

There are a couple ways to do this. Using the parse() is one. Manipulating the set() is another. Instantiating these in your initialize() is another one. Doing it all outside of the Path model is another (e.g. path = new Path(); path.nodes = new NodeCollection(); etc.)

A second consideration is this. Do you want the nodes and edge collections to be model attributes? Or model properties?

Oh so many choices. A lot of freedom but sometimes (to our frustration) it makes it more difficult to determine the "right way."

Since this comes up kind of often, I'm going to make a long post and go through these one by one. So bear with me as I continue to update this answer.

Doing it outside of the models - simple and straight-forward

This is usually an easy way to add nested models and collections when you only need it on a particular model or collection.

path = new PathModel();

path.nodes = new NodeCollection();
path.edge = new EdgeCollection();

// Continue to set up the nested data URL, etc.

This is the simplest way and works well when you're dealing with one time models and collections that don't need to have a definition. Although you could easily produce these models in some method (e.g. view method) that constructs this object before doing anything with it.

Using initialize() Sub-model / collections in every model

If you know that every instance of a certain model will always have a sub-model or sub-collection, the easiest way to set things up would be to utilize the initialize() function.

For example, take your Path model:

Path = Backbone.Model.extend({
    initialize: function() {
        this.nodes = new NodeCollection();
        this.paths = new PathCollection();

        // Maybe assign a proper url in relation to this Path model
        // You might even set up a change:id listener to set the url when this
        // model gets an id, assuming it doesn't have one at start.
        this.nodes.url = this.id ? 'path/' + this.id + '/nodes' : undefined;
        this.paths.url = this.id ? 'path/' + this.id + '/paths' : undefined;
    }
});

Now your sub-collections can be fetched like path.nodes.fetch() and it will route to the correct URL. Easy peasy.

Using parse() for instantiating and setting sub-data

Perhaps, it gets a little more tricky if you don't want to assume every model is going to have a nodes and edge collections. Maybe you want nested models/collections only if the fetch() sends back such data. This is the case where using parse() can come in handy.

The thing with parse() is that it takes ANY json server response and can properly namespace and deal with it before passing it to the model set() function. So, we can check to see if a model or collection raw data is included and deal with it before reducing the response down to parent model attributes.

For example, maybe from our server we get this response:

// Path model JSON example with nested collections JSON arrays
{
    'name':'orange site',
    'url':'orange.com',
    'nodes':[
        {'id':'1', 'nodeColor':'red'},
        {'id':'2', 'nodeColor':'white'},
        {'id':'3', 'nodeColor':'blue'}
    ],
    'edge':[
        {'id':'1', 'location':'north'},
        {'id':'1', 'location':'south'},
        {'id':'1', 'location':'east'}
    ]
}

With the default parse() Backbone will gobble this up and assign your path model attributes 'nodes' and 'edge' with an array() of data (not collections.) So we want to make sure that our parse() deals with this appropriately.

parse: function(response) {

    // Check if response includes some nested collection data... our case 'nodes'
    if (_.has(response, 'nodes')){

         // Check if this model has a property called nodes
        if (!_.has(this, 'nodes')) {  // It does not...
            // So instantiate a collection and pass in raw data
            this.nodes = new NodeCollection(response.nodes);
        } else {
            // It does, so just reset the collection
            this.nodes.reset(response.nodes);
        }

        // Assuming the fetch gets this model id
        this.nodes.url = 'path/' + response.id + '/nodes';  // Set model relative URL

        // Delete the nodes so it doesn't clutter our model attributes
        delete response.nodes;
    }

    // Same for edge...

    return response;
}

You can also use a customized set() to deal with your sub-data. After much going back and forth between which is better, manipulating set() or doing it in parse() I've decided that I like to use parse() more. But I'm open to other people's thoughts on this.

Using set() to deal with your sub-data

While parse() relies on either fetching the data or passing data into a collection with the option parse:true some people find it preferential to change the set() function. Again, I'm not sure there is a correct choice but here is how that would work.

set: function(attributes, options) {
    // If we pass in nodes collection JSON array and this model has a nodes attribute
    // Assume we already set it as a collection
    if (_.has(attributes, 'nodes') && this.get("nodes")) {
        this.get('nodes').reset(attributes.nodes);
        delete attributes.nodes;
    } else if (_.has(attributes, 'nodes') && !this.get('nodes')) {
        this.set('nodes', new NodeCollection(attributes.nodes));
        delete attributes.nodes;
    }

    return Backbone.Model.prototype.set.call(this, attributes, options);
}

So if we already have an attribute and it is a collection, we reset() it. If we have an attribute but it isn't a collection, we instantiate it. It's important to make sure you correctly translate the JSON array of sub-data into a collection before you pass it to the prototype set(). Backbone, doesn't interpret the JSON array as a collection and you'll only get a straight-up array.

So in a nut shell, you got many many options on how to go about. Again, currently I favor a mix of using initialize() when I know something will always have those sub-models/collections and parse() when the situation only calls for possible nested data on fetch() calls.

Regarding that question of yours... (Oh yeah, there was a question)

You can allow Path to instantiate sub-models from a hash in a variety of ways. I just gave you 4. You CAN use parse if you want, if you know you're going to be fetch() the path model OR maybe even a pathCollection... pathCollection.fetch({parse:true}) Is there a convention? Maybe maybe not. I like to use a combination of ways depending on the context in which I think I'll be using the models/collections.

I'm very open to discussion on some of these practices and whether they are good or bad. They are just many solutions I've come across on Stack and incorporated into my own working habits and they seem to work just fine for me. :-)

Get yourself a coffee and a pat on the back, that was a long read.

like image 77
jmk2142 Avatar answered Sep 21 '22 01:09

jmk2142