Backbone Nestify is a Backbone.js plugin for nesting Backbone Models and Collections. It depends only on Backbone and Underscore.
- 0.2.0 release - minified, 4kb
- 0.2.0 release - 23 kb
Backbone Nestify provides two features:
- A syntax to more easily nest, and access, attributes within nested Models and Collections.
var item3 = shoppingCart.get("items|3");
// returns the nested item Model instance
shoppingCart.set("items|3|id", 50]);
// Third item now has an id of 50
- An API to specify the nesting that should take place.
var spec = {
"account":AccountModel,
"items":ItemCollection,
};
var mixin = nestify(spec);
var ShoppingCartModel = Backbone.Model.extend(mixin);
At it's most basic: you provide nestify with a spec and receive a mixin.
var spec = {...};
var mixin = nestify(spec);
var MyModel = Backbone.Model.extend(mixin);
The mixin can be added to any Model definition or instance.
For this and following examples, let's say you have JSON like this:
var shoppingCartJSON =
{pending:
{orderID:null,
items:[{itemID:"bk28",
qty:25,
desc:"AA batteries"}]},
account:
{acctID:55,
uname:"bmiob",
orders:[
{orderID:1,
items:[{itemID:"cc01",
qty:2,
desc:"meatball"},
{itemID:"cc25",
qty:87,
desc:"rhubarb"}]},
{orderID:2,
items:[{itemID:"sd23",
desc:"SICP"}]}
]}
};
To work with this, you could spec out model nesting as follows:
/*
a ShoppingCart has:
- "pending" => Order 0..1
- "account" => Account 1
an Account has:
- "orders" => [Order] 0..*
an Order has:
- "items" => [Item] 0..*
*/
// nestable Model, Collection definitions
var Item = Backbone.Model;
var Items = Backbone.Collection.extend({model:Item});
var Order = Backbone.Model.extend(nestify({
'items': Items
}));
var Orders = Backbone.Collection.extend({model:Order});
var Account = Backbone.Model.extend(nestify({
'orders': Orders
}));
var ShoppingCart = Backbone.Model.extend(nestify({
'pending': Order,
'account': Account
}));
With these Model and Collection definitions in place, a Model instance for this JSON can be constructed in the usual Backbone manner:
var shoppingCart = new ShoppingCart(shoppingCartJSON);
// or alternatively...
shoppingCart.set(shoppingCartJSON);
Nested attributes can be accessed with the nestify syntax (which is configurable).
var anItem = shoppingCart.get("account|orders|1|items|0");
expect(anItem).to.be.an.instanceof(Item);
expect(anItem.get("itemID")).to.equal("sd23");
Nestify customizes the Backbone Model the get and set methods to provide a convenient syntax to access the nested attributes of a nestified Model.
Nested attributes can be accessed using an array syntax, where each item in the array corresponds with a level of nesting:
shoppingCart.get(["pending","items",0,"itemID"]);
Or there is an equivalent stringified syntax using a delimited string:
shoppingCart.get("pending|items|0|itemID");
(The delimiter is configurable.)
shoppingCart.get("pending.items.0.itemID", {delim:"."});
Both of the above are shorthand for the manual way of accessing nested attributes using ordinary Backbone syntax:
shoppingCart.get("pending")
.get("items")
.at(0)
.get("itemID");
Nested attributes can be set with the array syntax:
shoppingCart.set(["pending","items",0,"itemID"], "abc123");
...or the (configurable) delimited string syntax:
shoppingCart.set("pending,items,0,itemID", "abc123", {delim: ",");
And in fact both of these are equivalent to just setting JSON on the model:
shoppingCart.set({pending: {items: [{itemID:"abc123"}]}});
Backbone Model's unset and clear methods are both supported. The unset function supports nestify's extended syntax:
shoppingCart.unset("orders|0|items|2|backOrdered");
The plugin provides an API which accepts a nesting spec. The resulting mixin can then be set on individual Model instances or Model class constructors. Broadly speaking, the spec is a mapping from Model attribute names to the nested Model or Collection class for those attributes.
In other words, a Model (or Collection) definition (or instance) is nestified once, up front. Thereafter, any modifications to instances of that Model will adhere to the nesting specification. This is particularly good for dynamically deserializing complex JSON into the proper tree of nested Model/Collection instances.
In this illustration, a ShoppingCart Model is nestified. Two of its attributes are paired with the desired nested Model types (Order and Account, respectively).
var Order = Backbone.Model.extend(...
Account = Backbone.Model.extend(...
var shoppingCartSpec = {
'pending': Order,
'account': Account
};
var shoppingCartMixin = nestify(shoppingCartSpec);
var ShoppingCart = Backbone.Model.extend(shoppingCartMixin);
A more typical scenario is to combine the mixin with other custom properties when defining a Model definition. (Note the use of Underscore's extend function.)
var ShoppingCart = Backbone.Model.extend(_.extend({
// ...custom properties...
}, shoppingCartMixin));
Once nestified, the ShoppingCart Model is ready to be used.
var cart = new ShoppingCart();
cart.set("pending|id", 5);
cart.get("pending"); // returns an Order instance with an 'id' of 5
The mixin, once produced, can be added to a Backbone Model or Collection definition or instance using any of the ordinary means provided by Backbone. It can be included in the Model definition like so:
var ShoppingCart = Backbone.Model.extend(_.extend({
// ...model definition...
}, mixin));
Which would be equivalent to modifying the prototype of an existing Model (or Collection) constructor function:
_.extend(ShoppingCart.prototype, mixin);
It can be mixed in directly to a Model or Collection instance, if need be.
var shoppingCart = new Backbone.Model();
_.extend(shoppingCart, mixin);
Nestifying is not confined to the top-level Model only. The nested Model and Collection types can themselves be nestified (as can be seen in the Example).
Nestify options, like Backbone options, are a simple hash of name/value pairs. They can be specified by either of the following means:
- When calling nestify(), pass an options hash as a second (optional) parameter. These options are in effect for the lifetime of the mixin.
var mixin = nestify(spec, {delim:"."});
- Piggybacking on Backbone options when calling get() or set(). These options are only in effect for the duration of the method call; they will override any options specified to the nestify() function.
shoppingCart.get("pending.items.0.itemID", {delim:"."});
The delim
option can be used to specify the delimeter to use in the stringified syntax. By default this delimiter is the pipe character |
.
shoppingCart.get("pending|items|0|itemID");
// or
shoppingCart.get("pending.items.0.itemID", {delim:"."});
The update
option gives fine-grained control over the updating of Nestify containers. The possible values are reset
, merge
, and smart
.
reset
- the contents of a container is completely replacedmerge
- new values are merged into a container's current valuessmart
- new values are "smart"-merged into a container's current values.
Each option has slightly different implications for each different type of container.
Note: Currently, the contents of Objects and Arrays are not recursively updated. That is, containers nested within them are not intelligently updated, but rather are left alone or replaced altogether.
This option is best illustrated with examples; let's start with an existing order.
var order = new Order({items:[{id:1,desc:"bread"},
{id:2,desc:"cheese"}]});
Each following section contains an example which updates this order.
Containers' contents are replaced completely:
Collection
- updated using reset, which completely replaces its contents.Model
- cleared, then updated using setArray
- default behavior - any existing Array is replaced by the new ArrayObject
- default behavior - any existing Object is replaced by the new Object
Example: resetting the Items Collection...
order.set({items:[{id:3,desc:"butter"}]}, {update:"reset"});
...results in the order now having a single 'butter' Item.
The most precise behavior: container attributes are updated by index for Array-like containers, and by attribute-name for Object-like containers.
Collection
- default behavior - values are overwritten individually, in place, by index (see at method)Model
- default behavior - updated using setArray
- updated by numerical index. Note: Currently, the contents of Arrays are not recursively merged. That is, containers nested within the Array are not intelligently updated, but rather are left alone or replaced altogether.Object
- updated by String attribute name.Note: Currently, the contents of Objects are not recursively merged. That is, containers nested within the Object are not intelligently updated, but rather are left alone or replaced altogether.
Example: updating the Items Collection...
order.set({items:[{id:3,desc:"butter"}]}, {update:"merge"});
...will replace the first Item in the Collection (bread) with a new first Item (butter). The second item could be replaced instead (note the null
in the array):
order.set({items:[null,{id:3,desc:"butter"}]}, {update:"merge"});
...which would be equivalent to using the nestify syntax:
order.set("items|1|", {id:3,desc:"butter"});
Indicates a "smart" merge. For Collections, a "smart" update is performed. For all other containers this option is the same as using update:merge. See the section on containers for a full explanation.
Behavior, by container type:
Collection
- updated using its set method, which performs a Backbone "smart" update. (See documentation for additional Backbone options that can be paired with this one.)Model
- same as mergeArray
- same as mergeObject
- same as merge
Example: setting the Items and taking advantage of Backbone's {remove:false}
option...
order.set({items:[{id:3,desc:"butter"}]}, {update:"smart", remove:false});
...will do a Collection smart merge, resulting in the order now having all three Items.
Conceptually speaking, a container is anything that can hold a nested value. It is a Model attribute which Nestify can use to nest attributes. A container can be any of:
- a
Backbone Model
- a
Backbone Collection
- a plain
Array
- a plain
Object
All containers can be indexed using the getter/setter syntax. Notice that two of these container types are Array
-like: Collections
and Arrays
. They both are indexed by integer numbers. Similarly, the other two container types are Object
-like: Models
and Objects
. They both are indexed by String attribute names.
Controlling the updating of a Model's nested containers is fundamental to Nestify. Nestify provides the update option to control the updating of container instances. Nestify also allows this update policy to be set on a container-by-container basis using advanced container specification. Finally, Nestify assumes reasonable defaults for each type of container:
Collection
- default update behavior is mergeModel
- default update behavior is mergeArray
- default update behavior is reset (same as unmodified Backbone)Object
- default update behavior is reset (same as unmodified Backbone)
Backbone itself already allows simple nesting via native JavaScript Arrays and Objects; Nestify simply provides its getter/setter syntactic sugar on top of this. In fact, using an empty spec such as this:
var Model = Backbone.Model.extend(nestify());
...will still nestify the Model and allow the use of Nestify's getter/setter syntax. It simply will not change the storage of those nested attributes; they will continue to be stored in plain Objects or Arrays.
var m = new Model({
orders: [{id: 1}]
});
m.get("orders|0|id"); //returns 1
m.get("orders|0"); //returns an Object
m.get("orders"); //returns an Array
Specifying that a container should be a Backbone Model or Collection is the expected majority use case.
var spec = {account: AccountModel,
orders: OrdersCollection}}
In the examples so far, the spec has always been a simple hash of Model attributes to nested Model or Collection container types. This abbreviated form is expected to be the typical, 80% use case.
var shoppingCartSpec = {
'pending': Order,
'account': Account
};
But it is possible to opt-in to a more verbose but powerful general spec form.
The general spec form has the following structure (in pseudo-BNF):
// The full-blown, general 'spec' is actually an array of specs.
<speclist> ::= [ spec ]
<spec> ::= { match: <matcher>, /* optional; omitted means 'match
any/all attribute names' */
container: <container> }
| { hash: <hash> }
<hash> // this is just the abbreviated 'hash' spec form
<matcher> ::= String
| RegExp
| Function // predicate
<container> ::= <constructor>
| {constructor: <constructor>,
args : <arguments to constructor>, // optional
spec : <speclist> // optional
<constructor> ::= <Backbone Model constructor function>
| <Backbone Collection constructor function>
| <Array constructor function>
| <Object constructor function>
| <arbitrary function>
By passing an array to Nestify, you are opting-in to the advanced spec form; you cannot use the abbreviated hash form. But, you can still explicitly supply a hash thusly:
// advanced form, explicit 'hash'
var spec = [{hash: {account: AccountModel,
orders: OrdersCollection}}];
// equivalent to abbreviated form:
var spec = {account: AccountModel,
orders: OrdersCollection}};
A hash can be thought of as the degenerate form of the more general, more powerful pairing of matchers and a containers.
An alternative to the hash is a matcher/container pair. A matcher can be used to match on any of the containing Model's attributes; The container specifies what sort of object should be stored for that attribute, to contain nested attributes.
A String matcher implies doing a ===
match on the attribute name.
{match: "foo",
container: FooModel}
// equivalent to
{hash: {'foo': FooModel}}
A JavaScript RegExp can be used as a matcher for more powerful attribute matching.
{match: /ord/,
container: OrderModel}
For maximum matching capability, a JavaScript predicate Function can be used.
var len=3;
//...
{match: function(attr, val, existing, opts){
return attr.length === len;
},
container: OrderModel}
The supplied predicate will be passed these parameters:
- The String
attribute
name - The incoming, unmodified container
value
to be set - The
existing
container, if any - The
opts
hash
It should return true or false.
The matcher can be omitted entirely; this means "match all attributes".
// will match everything
{container: EverythingModel}
The basics of containers have already been covered. The general spec form introduces a new concept, a constructor, which provides more flexibility in generating new container values.
A constructor is a JavaScript function which produces a container value. A constructor function can be any container constructor function or a custom (non-constructor) function.
So far, examples have only shown an implied constructor value by pairing the container
attribute with a Backbone Model or Collection constructor:
{orders: OrdersCollection}
This is equivalent to the general form, which makes explicit the constructor:
{match: 'orders',
container:
{constructor: OrdersCollection}
}
Specifying that a container should be a Backbone Model or Collection is the expected majority use case; just use their constructor functions, like so:
var spec = [{match: "account",
container: AccountModel},
{match: "orders",
container: OrdersCollection}];
// equivalent to:
var spec = [{hash: {account: AccountModel,
orders: OrdersCollection}}];
// and
var spec = {account: AccountModel,
orders: OrdersCollection}}
Specifying arguments to the Backbone constructor, and/or specifying a Nestify spec for instances of that Backbone container, can be accomplished using the constructor
attribute:
var spec = [{match: "account",
container: {constructor: AccountModel,
args: {preferred: true},
spec: {rewards: RewardCollection}
}];
For utmost flexibility, a constructor can be a custom function.
The supplied function will be passed five parameters:
- The incoming, unmodified container
value
to be set (for example, raw JSON) - The
opts
hash - The String
attribute
name - The containing Backbone
Model
The function should return the resulting container object.
var spec = [{match: "account",
container: function(v, opts, att, m){
return new AmazingModel(v, opts);
}
}];
// or...
var spec = [{match: "account",
container: {
constructor: function(v, opts, att, m){
return new AmazingModel(v, opts);
}
}
}];
The function will not be invoked using the new
keyword.
An example of the Advanced Spec Form:
nestify([{
hash: {foo: FooModel,
bar: BarModel}
},{
match: /abc/,
container: BarModel
},{
match: function(...){return true;},
container: {
constructor: function(...){return something;}
}
},{
// default case, no 'matcher'
container: {
constructor: BazModel,
args: {argle:"bargle"},
spec: [...BazModel's own spec...]
}
}],{ // optional 'opts' arg
delim: "."
});
The plugin works by replacing the get and set methods of the Model (or Collection) definitions. The replacement methods delegate to the original methods to provide the usual Backbone functionality, and they add the additional functionality provided by this plugin. The mixin is just an ordinary object containing these two methods.
var mixin = nestify({
...
});
_.keys(mixin); // ["get","set"]
I'll be honest here: like countless software developers before us, we identified a problem and, not knowing much about it yet, assumed it would be easy to fix by ourselves. Nesting attributes within Backbone Models is apparently a popular enough need that it merits its own FAQ. We evaluated some of the existing plugins but decided for various reasons to try our own approach, which eventually became this plugin.
Having said all of that, we believe Nestify fills a couple of really sweet spots:
- It is mixin-based rather than Class based. That is, your Models do not have to extend a particular Model superclass in order to use the plugin. Instead, the plugin produces a mixin object which can be added to any existing Model or Collection definition, or even just a single instance of a type of Model or Collection.
- a simple but flexible getter/setter syntax.
- Nestify was designed especially to make serialization to and from JSON work seamlessly. In our case, we have a RESTful API returning potentially complicated and deeply-nested responses, and we want our Model instances to "just work" once they are configured.
$ npm install
$ grunt [dist]