Tiny model relations with Backbone.associate

Several months ago I outlined a minimal implementation for model associations in Backbone. As my projects at the time had little concern for their initial footprint and large, fully-featured relational libraries are everywhere, the initial sketch was mostly self-rumination–the what-ifs of minimal data relations.

But there comes a time that size matters, where a few hundred milliseconds can make or break conversion rates and retention. With that need in mind, the original musings have been wrapped in a liberal dollop of OSS goodness and released on github for use and contribution as Backbone.associate.

Weighing in at a cool 0.6k (uglified and gzipped), associate provides the bare minimum of data relations, and nothing else. No complicated hierarchies. No identity management. But consistent, accessible, spec’d and tested relations.

A really simple case

Let’s use Backbone.associate to describe the relationships between a couple of models and a collection.

var Country = Backbone.Model.Extend({ /* ... */ });
var City = Backbone.Model.Extend({ /* ... */ });
var Cities = Backbone.Collection.extend({ model: City });

If we wanted to model a country’s relationship with its capital and a collection of other cities, we could write an associate, pass in data, and–voila! The relationship is defined:

Backbone.associate(Country, {
  capital: { type: City },
  cities: { type: Cities }
});

var germany = new Country({
  capital: { name: 'Berlin' },
  cities: [
    { name: 'Stuttgart' },
    { name: 'Leipzig' },
    { name: 'Hannover' }
  ]
}, { parse: true });

germany.capital() // A City named "Berlin"
germany.cities() // A collection of Cities

How it works

Not every application that uses data relations will need to maintain an identity map or have sweet syntactic sugar for accessing content in related models. Still, even the most minimal model relationships must be able to:

  • traversing between related data
  • inflate and serialize data with relationships intact

For backbone’s case, that means ensuring that relationships:

  • can be parsed from the content retrieved by Backbone.sync
  • accessed by their relatives
  • serialized back into a form that can be stored for the future

Backbone.associate handles these chores in three steps. First, the parse method of a model sent to associate is patched to filter data corresponding to any of the models relations. If a match is found, the relations are created, passed the data, and added by reference to the original model’s attribute hash. If no data is included in the initial content, worry not: a fresh instance of the associated class will simply be built instead.

Once the data has been parsed, the related models may be retrieved from the parent model’s attribute hash either directly or by using the named accessors automatically added to the model prototype by Backbone.associate:

relation = MyModel.get('otherModel');
relation = MyModel.otherModel();

Finally, whenever the application needs to save the model state, a wrapper around Backbone.toJSON ensures that each related model will be serialized as it is encountered.

The relationships

Backbone.associate treats all relations as one-to-one connections between two objects, with the type of relationship (has-one or has-many) being determined by the related object itself. The only real assumption about the object on the other end is that it is that it will either be a Backbone Model, Backbone Collection, or something that closely resembles them. As long as a constructor accepts data, associate will pass it. Boom. Done.

And now, the catches

Being a minimal implementation, Backbone.associate leaves the implementation of standard features like event bubbling, reverse relations, nested assignment and URLs are left to the application’s discretion. It isn’t a good choice for applications that need full features with little time for custom development.

There’s also some annoying fallout from using Backbone.parse to inflate relations. Since parse is intended for use with server data, it will not be called during model initialization unless the initialize method is explicitly told to call it:

new MyClass(data, { parse: true });

Conclusion

Like everything, associate is a work in progress. There are doubtless cases that haven’t been tested, code that should be written, code that should be re-written, and code that should be dropped. The “wrap-everything” approach that allows associate to operate without requiring inheritance is particularly dangerous, but its seamless behavior may be difficult to replace. As always, contributions are welcome; fork, contribute, or comment below and I’ll do my best to respond in a timely manner!

Resources

Featured