Socket
Socket
Sign inDemoInstall

backbone.nested-types

Package Overview
Dependencies
2
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.5.0 to 0.7.1

671

nestedtypes.js

@@ -1,3 +0,3 @@

// Backbone.nestedTypes 0.2 (https://github.com/Volicon/backbone.nestedTypes)
// (c) 2014 Vlad Balin, may be freely distributed under the MIT license
// Backbone.nestedTypes 0.7.1 (https://github.com/Volicon/backbone.nestedTypes)
// (c) 2014 Vlad Balin & Volicon, may be freely distributed under the MIT license

@@ -12,3 +12,4 @@ ( function( root, factory ){

else{
factory( root, root.Backbone, root._ );
root.NestedTypes = {};
factory( root.NestedTypes, root.Backbone, root._ );
}

@@ -19,21 +20,37 @@ }( this, function( exports, Backbone, _ ){

function attachNativeProperties( This, properties, Base ){
_.each( properties, function( propDesc, name ){
var prop = typeof propDesc === 'function' ? {
get: propDesc,
enumerable: false
} : propDesc;
var error = {
propertyConflict : function( context, name ){
console.error( '[Type error](' + context.__class + '.extend) Property ' + name + ' conflicts with base class members' );
},
if( name in Base.prototype ){
throw new TypeError( 'extend: property ' + name + ' conflicts with base class members!' );
}
argumentIsNotAnObject : function( context, value ){
console.error( '[Type Error](' + context.__class + '.set) Attribute hash is not an object:', value, 'In model:', context );
},
Object.defineProperty( This.prototype, name, prop );
});
}
unknownAttribute : function( context, name, value ){
console.error( '[Type Error](' + context.__class + '.set) Attribute "' + name + '" has no default value.', value, 'In model:', context );
},
function extendWithProperties( Base ){
defaultsIsFunction : function( context ){
console.error( '[Type Error](' + context.__class + '.defaults] "defaults" must be an object, functions are not supported. In model:', context );
}
};
function createExtendFor( Base ){
return function( protoProps, staticProps ){
var This = extend.call( this, protoProps, staticProps );
attachNativeProperties( This, protoProps.properties, Base );
_.each( protoProps.properties, function( propDesc, name ){
var prop = _.isFunction( propDesc ) ? {
get: propDesc,
enumerable: false
} : propDesc;
if( name in Base.prototype ){
error.propertyConflict( This.prototype, name );
}
Object.defineProperty( This.prototype, name, prop );
});
return This;

@@ -43,9 +60,15 @@ };

/*************************************************
NestedTypes.Class
- can be extended as native Backbone objects
- can send out and listen to backbone events
- can have native properties
**************************************************/
exports.Class = function(){
function Class(){
this.initialize.apply( this, arguments );
};
}
_.extend( Class.prototype, Backbone.Events, { initialize: function (){} } );
Class.extend = extendWithProperties( Class );
_.extend( Class.prototype, Backbone.Events, { __class: 'Class', initialize: function (){} } );
Class.extend = createExtendFor( Class );

@@ -55,65 +78,21 @@ return Class;

exports.Collection = function(){
var Collection,
CollectionProto = Backbone.Collection.prototype;
/*************************************************
NestedTypes.Model
- extension of Backbone.Model
- creates native properties for attributes
- support optional type specs for attributes
- perform dynamic types coercion and checks
- support nested models and collections with 'change' events bubbling
- transparent typed attributes serialization and deserialization
**************************************************/
function wrapCall( func ){
return function(){
if( !this.__changing++ ){
this.trigger( 'before:change' );
}
func.apply( this, arguments );
if( !--this.__changing ){
this.trigger( 'after:change' );
}
};
}
Collection = Backbone.Collection.extend({
triggerWhenChanged: 'change add remove reset sort',
deepClone: function(){
var copy = CollectionProto.clone.call( this );
copy.reset( this.map( function( model ){
return model.deepClone();
} ) );
return copy;
},
__changing: 0,
set: wrapCall( CollectionProto.set ),
remove: wrapCall( CollectionProto.remove ),
add: wrapCall( CollectionProto.add ),
reset: wrapCall( CollectionProto.reset ),
sort: wrapCall( CollectionProto.sort )
});
Collection.extend = extendWithProperties( Collection );
return Collection;
}();
exports.Model = function(){
var extend = Backbone.Model.extend,
ModelProto = Backbone.Model.prototype;
var ModelProto = Backbone.Model.prototype,
originalSet = ModelProto.set,
primitiveTypes = [String, Number, Boolean];
function delegateEvents( name, oldValue, newValue ){
var replace = false;
oldValue && this.stopListening( oldValue );
if( _.isEqual( oldValue, newValue ) ){
return;
}
if( oldValue && oldValue.triggerWhenChanged ){
replace = true;
this.stopListening( oldValue );
}
if( newValue && newValue.triggerWhenChanged ){
replace = true;
if( newValue ){
this.listenTo( newValue, 'before:change', onEnter );

@@ -123,3 +102,3 @@ this.listenTo( newValue, 'after:change', onExit );

this.listenTo( newValue, newValue.triggerWhenChanged, function(){
var value = this.get( name );
var value = this.attributes[ name ];

@@ -131,5 +110,5 @@ if( this.__duringSet ){

this.attributes[ name ] = null;
ModelProto.set.call( this, name, value );
originalSet.call( this, name, value );
}
} );
});

@@ -142,48 +121,84 @@ _.each( this.listening[ name ], function( handler, events ){

if( replace ){
this.trigger( 'replace:' + name, this, newValue, oldValue );
}
this.trigger( 'replace:' + name, this, newValue, oldValue );
}
function typeCast( Ctor, name, value ){
var oldValue = this.attributes[ name ],
valueHasOtherType = ( value != null ) && !( value instanceof Ctor ),
newValue;
function typeCastBackbone( ModelOrCollection, name, value, options ){
var incompatibleType = value != null && !( value instanceof ModelOrCollection ),
existingModelOrCollection = this.attributes[ name ];
if( oldValue && oldValue.set && valueHasOtherType ){
oldValue.set( value );
newValue = oldValue;
if( incompatibleType ){
if( existingModelOrCollection ){ // ...delegate update for existing object 'set' method
existingModelOrCollection.set( value, options );
value = existingModelOrCollection;
}
else{ // ...or create a new object, if it's not exist
value = new ModelOrCollection( value, options );
}
}
else{
newValue = valueHasOtherType ? new Ctor( value ) : value;
delegateEvents.call( this, name, oldValue, newValue );
if( value !== existingModelOrCollection ){
delegateEvents.call( this, name, existingModelOrCollection, value );
}
return newValue;
return value;
}
function onExit( a_attrs, options ){
var attrs = a_attrs || {};
function onExit( attrs, options ){
if( !--this.__duringSet ){
_.each( this.__nestedChanges, function( value, name ){
if( !( name in attrs ) ){
attrs[ name ] = value;
}
attrs || ( attrs = {} );
for( var name in this.__nestedChanges ){
name in attrs || ( attrs[ name ] = this.__nestedChanges[ name ] );
if( attrs[ name ] === this.attributes[ name ] ){
this.attributes[ name ] = null;
}
}, this );
}
this.__nestedChanges = {};
}
ModelProto.set.call( this, attrs, options );
}
attrs && originalSet.call( this, attrs, options );
}
function onEnter(){
if( !this.__duringSet++ ){
this.__nestedChanges = {};
this.__duringSet++ || ( this.__nestedChanges = {} );
}
function setMany( attrs, options ){
var types = this.__types, Ctor, value;
if( attrs.constructor !== Object ){
error.argumentIsNotAnObject( this, attrs );
}
onEnter.call( this );
// cast values to default types...
for( var name in attrs ){
Ctor = types[ name ],
value = attrs[ name ];
if( Ctor ){
if( Ctor.prototype.triggerWhenChanged ){ // for models and collections...
attrs[ name ] = typeCastBackbone.call( this, Ctor, name, value, options );
}
else if( value != null ){
if( primitiveTypes.indexOf( Ctor ) > -1 ){ // use primitive types as is
attrs[ name ] = Ctor( value );
}
else if( !( value instanceof Ctor ) ){ // use constructor to convert to default type
attrs[ name ] = new Ctor( value );
}
}
}
else if( Ctor !== null ){
error.unknownAttribute( this, name, value );
}
}
// apply changes
onExit.call( this, attrs, options );
return this;
}

@@ -196,32 +211,51 @@

__defaults: {},
__types: {},
__types: { id: null },
__class : 'Model',
set: function( first, second, third ){
// handle different call options...
var attrs, options, types = this.__types;
isValid : function( options ){
return ModelProto.isValid( options ) && _.every( this.attribute, function( attr ){
if( attr && attr.isValid ){
return attr.isValid( options );
}
else if( attr instanceof Date ){
return attr.getTime() !== NaN;
}
else{
return attr !== NaN;
}
});
},
if( typeof first === 'string' ){
( attrs = {} )[ first ] = second;
options = third;
set: function( name, value, options ){
if( typeof name !== 'string' ){
return setMany.call( this, name, value );
}
else{
attrs = first;
options = second;
}
onEnter.call( this );
// optimized set version for single argument
var Ctor = this.__types[ name ];
// cast values to default types...
_.each( attrs, function( value, name ){
var Ctor = types[ name ];
if( Ctor ){
if( Ctor.prototype.triggerWhenChanged ){
var attrs = {};
if( Ctor ){
attrs[ name ] = typeCast.call( this, Ctor, name, value );
onEnter.call( this );
attrs[ name ] = typeCastBackbone.call( this, Ctor, name, value, options );
onExit.call( this, attrs, options );
return this;
}
}, this );
else if( value != null ){
if( primitiveTypes.indexOf( Ctor ) > -1 ){ // use primitive types as is
value = Ctor( value );
}
else if( !( value instanceof Ctor ) ){ // use constructor to convert to default type
value = new Ctor( value );
}
}
}
else if( Ctor !== null ){
error.unknownAttribute( this, name, value );
}
// apply changes
onExit.call( this, attrs, options );
return this;
return originalSet.call( this, name, value, options );
},

@@ -231,11 +265,9 @@

deepClone: function(){
var copy = ModelProto.clone.call( this );
var attrs = {};
_.each( copy.attributes, function( value, key ){
if( value && value.deepClone ){
copy.set( key, value.deepClone() );
}
} );
_.each( this.attributes, function( value, key ){
attrs[ key ] = value && value.deepClone ? value.deepClone() : value;
});
return copy;
return new this.constructor( attrs );
},

@@ -246,9 +278,7 @@

toJSON: function(){
var res = ModelProto.toJSON.apply( this, arguments );
var res = {};
_.each( res, function( value, key ){
if( value && value.toJSON ){
res[ key ] = value.toJSON();
}
} );
_.each( this.attributes, function( value, key ){
res[ key ] = value && value.toJSON ? value.toJSON() : value;
});

@@ -261,56 +291,161 @@ return res;

function parseDefaults( spec, Base ){
var defaults = _.defaults( spec.defaults || {}, Base.prototype.__defaults ),
fnames = _.functions( defaults ),
values = _.omit( defaults, fnames ),
types = _.pick( defaults, fnames );
// Attribute metatype
// ------------------
function Attribute( spec ){
if( 'typeOrValue' in spec ){
var something = spec.typeOrValue;
return _.extend( {}, spec, {
defaults : createDefaults( values, types ),
__defaults : defaults,
__types : types
});
}
if( _.isFunction( something ) ){
this.type = something;
}
else{
this.value = something;
}
}
else{
_.extend( this, spec );
function createDefaults( values, ctors ){
return function(){
var defaults = _.clone( values );
if( spec.get || spec.set ){
// inline property override...
this.property = function( name ){
return {
get : spec.get || function(){
return this.attributes[ name ];
},
_.each( ctors, function( Ctor, name ){
defaults[ name ] = new Ctor();
} );
set : spec.set || function( value ){
this.set( name, value );
return value;
},
return defaults;
};
enumerable : false
};
};
}
}
}
function createAttrPropDesc( name ){
Attribute.prototype.type = null;
Attribute.prototype.property = function( name ){
return {
get: function(){
get : function(){
return this.attributes[ name ];
},
set: function( val ){
this.set( name, val );
return val;
set : function( value ){
this.set( name, value );
return value;
},
enumerable: false
enumerable : false
};
};
Model.Attribute = function( spec ){
if( arguments.length == 2 ){
spec = {
type : arguments[ 0 ],
value : arguments[ 1 ]
};
}
return new Attribute( spec );
};
function parseDefaults( spec, Base ){
if( _.isFunction( spec.defaults ) ){
error.defaultsIsFunction( spec );
}
var defaults = _.defaults( spec.defaults || {}, Base.prototype.__defaults ),
idAttr = spec.idAttribute || Base.prototype.idAttribute,
attributes = {};
attributes[ idAttr ] = new Attribute( { value : undefined } );
if( idAttr === 'id' ){
attributes[ idAttr ].property = false;
}
_.each( defaults, function( attr, name ){
attr instanceof Attribute || ( attr = new Attribute({ typeOrValue: attr }) );
if( name in Base.prototype.__defaults ){
attr.property = false;
}
attributes[ name ] = attr;
});
return _.extend( {}, spec, {
__defaults : defaults, // needed for attributes inheritance
__attributes : attributes
});
}
function attachNativeProperties( This, spec ){
function createDefaults( attributes ){
var json = [], init = {}, cast = {}, refs = {};
_.each( attributes, function( attr, name ){
if( attr.value !== undefined ){
if( attr.type ){ // when type is specified...
if( attr.value === null ){ // null should be directly assigned
json.push( name + ':' + JSON.stringify( attr.value ) );
}
else{
refs[ name ] = attr.value; //otherwise, it's assigned by reference
if( !( attr.value instanceof attr.type ) ){ // and if value has incompatible type...
cast[ name ] = attr.type; // it must be type-casted
}
}
}
else{ // if no type information available...
// ...guess if value is literal
if( !attr.value || typeof attr.value !== 'object' || attr.value.constructor === Object || attr.value.constructor === Array ){
json.push( name + ':' + JSON.stringify( attr.value ) ); // and make a deep copy
}
else{ // otherwise, copy it by reference.
refs[ name ] = attr.value;
}
}
}
else{
attr.type && ( init[ name ] = attr.type );
}
});
var literals = new Function( 'return {' + json.join( ',' ) + '}' );
return function(){
var defaults = literals();
_.extend( defaults, refs );
for( var name in init ){
defaults[ name ] = new init[ name ]();
}
for( var name in cast ){
defaults[ name ] = new cast[ name ]( defaults[ name ] );
}
return defaults;
}
}
function createNativeProperties( This, spec ){
var properties = {};
if( spec.properties !== false ){
_.each( spec.defaults, function( notUsed, name ){
properties[ name ] = createAttrPropDesc( name );
_.each( spec.__attributes, function( attr, name ){
attr.property && ( properties[ name ] = attr.property( name ) );
} );
_.each( spec.properties, function( propDesc, name ){
properties[ name ] = typeof propDesc === 'function' ? {
properties[ name ] = _.isFunction( propDesc ) ? {
get: propDesc,
enumerable: false
} : propDesc;
} );
} : _.defaults( {}, propDesc, { enumerable : false } );
});

@@ -320,21 +455,201 @@ _.each( properties, function( prop, name ){

name === 'cid' || name === 'id' || name === 'attributes' ){
throw new TypeError( 'extend: attribute ' + name + ' conflicts with Backbone.Model base class members!' );
error.propertyConflict( This.prototype, name );
}
Object.defineProperty( This.prototype, name, prop );
} );
});
}
}
function extractTypes( attributes ){
var types = {};
_.each( attributes, function( attr, name ){
types[ name ] = attr.type;
});
return types;
}
Model.extend = function( protoProps, staticProps ){
var spec = parseDefaults( protoProps, this ),
This = extend.call( this, spec, staticProps );
var spec = parseDefaults( protoProps, this );
spec.defaults = createDefaults( spec.__attributes );
spec.__types = extractTypes( spec.__attributes );
attachNativeProperties( This, protoProps );
var This = extend.call( this, spec, staticProps );
createNativeProperties( This, spec );
return This;
};
var ModelReference = Model.extend({
__class : 'Model.RefTo',
model : null,
toJSON : function(){
return this.id;
},
parse : function( id ){
return { id : id };
}
});
Model.RefTo = function( collectionOrFunc ){
return Model.Attribute({
type : ModelReference,
property : function( name ){
return {
get : function(){
var ref = this.attributes[ name ];
if( !ref.model ){
var master = _.isFunction( collectionOrFunc ) ? collectionOrFunc.call( this ) : collectionOrFunc;
master && master.length && ( ref.model = master.get( name ) );
}
return ref.model;
},
set : function( model ){
var ref = this.attributes[ name ];
ref.model = model;
ref.id = model.id;
}
}
}
});
};
Model.Property = function( fun ){
return Model.Attribute({
type : exports.Class.extend({
toJSON : fun
}),
get : fun
});
};
return Model;
}();
exports.Collection = function(){
var Collection,
CollectionProto = Backbone.Collection.prototype;
function wrapCall( func ){
return function(){
if( !this.__changing++ ){
this.trigger( 'before:change' );
}
var res = func.apply( this, arguments );
if( !--this.__changing ){
this.trigger( 'after:change' );
}
return res;
};
}
Collection = Backbone.Collection.extend({
triggerWhenChanged: 'change add remove reset sort',
__class : 'Collection',
model : exports.Model,
isValid : function( options ){
return this.every( function( model ){
return model.isValid( options );
});
},
deepClone: function(){
var copy = CollectionProto.clone.call( this );
copy.reset( this.map( function( model ){
return model.deepClone();
} ) );
return copy;
},
__changing: 0,
set: wrapCall( CollectionProto.set ),
remove: wrapCall( CollectionProto.remove ),
add: wrapCall( CollectionProto.add ),
reset: wrapCall( CollectionProto.reset ),
sort: wrapCall( CollectionProto.sort )
});
Collection.extend = createExtendFor( Collection );
var refsCollectionSpec = {
triggerWhenChanged : "add remove reset sort",
__class : 'Collection.RefsTo',
isResolved : false,
toJSON : function(){
return _.pluck( this.models, 'id' );
},
parse : function( raw ){
var idName = this.model.prototype.idAttribute;
this.isResolved = false;
return _.map( raw, function( id ){
var res = {};
res[ idName ] = id;
return res;
});
},
set : function( models, options ){
_.extend( options, { merge : false } );
CollectionProto.set.call( this, models, options );
},
resolve : function( collection ){
var values = this.map( function( ref ){
return collection.get( ref.id );
});
this.reset( _.compact( values ), { silent : true } );
this.isResolved = true;
return this;
}
};
Collection.RefsTo = function( collectionOrFunc ){
return exports.Model.Attribute({
type : this.extend( refsCollectionSpec ),
property : function( name ){
return {
get : function(){
var refs = this.attributes[ name ],
master;
if( !refs.isResolved ){
master = _.isFunction( collectionOrFunc ) ? collectionOrFunc.call( this ) : collectionOrFunc;
master && master.length && refs.resolve( master );
}
return refs;
},
enumerable : false
}
}
});
};
return Collection;
}();
}));

@@ -17,4 +17,7 @@ {

},
"files": [
"nestedtypes.js"
],
"license": "MIT",
"version": "0.5.0"
"version": "0.7.1"
}
backbone.nestedTypes
====================
In case you're precisely know what you're looking for, it's backbone.js extension adding type annotations to model attributes, easiest possible way of dealing with nested models and collections, and native properties for attributes. Providing you with a more or less complete, simple, and powerful object system for JavaScript.
In case you're precisely know what you're looking for, it's backbone.js extension adding type annotations, type coercion and type checks (yes, _checks_) to model attributes, easiest possible way of dealing with nested models and collections, and native properties for attributes. Providing you with a more or less complete, simple, and powerful object system for JavaScript.

@@ -20,9 +20,16 @@ In case if you don't, here is a brief outline of problems we're solving with this little thing. There are two major goals behind:

3. Seriously reduce backbone.js painfullness and shorten learning curve for backbone newbies.
- Model and Collection's semantic is designed in the way, that it hard to make typical newbie errors. Some of them are not mistakes any more, but the right way to do things. For example, defaults section is automatically inherited for you from the base class, it's safe to forget 'get', etc...
- Minimum of new concepts and keywords are introduced. If you know backbone, you already know NestedTypes.
- Attribute types are checked and automatically coerced in run time.
These issues are addressed in many different backbone plugins, but this one is defferent.
We solve these problems encouraging you to type less, than you used to. 'Type specs' in model's 'defaults' section do all the magic. So, if your attribute is a date, just write in defaults, that it's Date. That's it. No, you don't need a compiler, it's still old good vanilla JS you're get used to.
We solve these problems encouraging you _to type less, than you used to_. Type specs in model's 'defaults' section do all the magic. So, if your attribute is a date, just write in defaults, that it's Date. That's it.
No, you don't need a compiler, or learn some non-standard syntax. It's still old good vanilla JS you're get used to.
Model's native properties
-------------------------
For any model attributes mentioned in 'defaults', use
Forget the 'get'. For any model attributes mentioned in 'defaults', use

@@ -93,3 +100,4 @@ ```javascript

New object will be created automatically for any typed attribute, no need to override `initialize`.
When typed attribute assigned with value of different type, constructor will be invoked to
When typed attribute assigned with value of different type, constructor will be invoked to
convert it to defined type.

@@ -111,6 +119,82 @@

It's important to note that defaults specs *are mandatory* with this plugin. Default implementation of Model.validate method will write an error in console if you attempt to save model with attributes which are not declared.
Also, defaults spec *must* be an object, at this time you can't use function as in plain Backbone. Support for them will be added later.
Type annotations and defaults rules summary
-----------------------------------
Semantic of type annonation designed to be both intuitive and protective, in order to have minimal learning curve and help to overcome typical backbone.js mistakes usually done by newbies. Howewer, it's not as simple behind the scene. Here's more formal and detailed description of logic behind defaults:
- defaults spec must be an object
- any function used as default value is treated as constructor, and will be invoked with 'new' to create an object.
- JSON literals used as defaults will be compiled to function and efficiently _deep_ _copied_.
- non-JSON values (other than direct instances of Object and Array) will be passed by reference. I.e. in this example:
```javascript
var M = Model.extend({
defaults : {
num : 1,
str : "",
arr : [ 1, 2, 3 ],
obj : { a: 1, b: 2 },
date : Date,
shared : new Date()
}
});
// creation of default values will behave exactly as the following code in plain backbone:
var _tmp = new Date();
var M = Model.extend({
defaults : function(){
return {
num : 1,
str : "",
arr : [ 1, 2, 3 ],
obj : { a: 1, b: 2 },
date : new Date(),
shared : _tmp
}
}
});
```
- type and default value may be specified separately. Standard type coercion rules will be applied to default values.
```javascript
var M = Model.extend({
defaults : {
date1 : Model.Attribute( Date, null ),
date2 : Model.Attribute( Date, '2012-12-12 12:12' )
}
});
// will behave as:
var M = Model.extend({
defaults : function(){
return {
date1 : null,
date2 : new Date( '2012-12-12 12:12' )
}
}
});
```
- Native getter and setter may be overriden for every attribute using full notation:
```javascript
var M = Model.extend({
defaults : {
date1 : Model.Attribute({
type : Date,
value : null,
get : function(){
var time = this.attributes.date1;
return time ? time : new Date( '1900-01-01 00:00' );
})
}
}
});
```
Nested Models and Collections
-----------------------------
To have nested models and collections, annotate attribute with Model or Collection type.
To use nested models and collections, just annotate attributes with Model or Collection type.

@@ -128,7 +212,5 @@ ```javascript

No need to override `initialize`, `parse`, and `toJSON`, everything will be done automatically.
No need to override `initialize`, `parse`, and `toJSON`, nested JSON will be parsed and generated automatically. You still can override parse to transform JSON received from the server, but there is no need to create new Model/Collection instances, because of the modified 'set' behaviour.
There is a difference from regular types in 'set' behaviour. If attribute's current value is not null,
and new value has different type, it will be delegated to 'set' method of nested model or collection.
I.e. this code:
If attribute is defined as Model or Collection, new value is an object or array (for example, JSON received form the server), and its current value is not null, it will be delegated to 'set' method of existig nested model or collection (!). If current value is null, new instance of model/collection will be created. I.e. this code:

@@ -154,2 +236,4 @@ ```javascript

'set' method for models and collection is *strictly type checked*. You'll got error in the console on every attempt to set values with incompatible type.
This mechanics of 'set' allows you to work with JSON from in case of deeply nested models and collections without the need to override 'parse'. This code (considering that nested attributes defined as models):

@@ -182,4 +266,62 @@

Nested collections of model's references
-------------------------------------------------
When you have many-to-many relationships, it is suitable to transfer such a relationships from server as arrays of model ids. NestedTypes gives you special attribute data type to handle such a situation.
```javascript
var User = Model.extend({
defaults : {
name : String,
roles : RolesCollection.RefsTo( rolesCollection ) // <- subclass of existing RolesCollection
}
});
var user = new User({ id: 0 });
user.fetch(); // <- and you'll receive from server "{ id: 0, name : 'john', roles : [ 1, 2, 3 ] }"
...
// however, user.roles behaves like collection of Roles.
assert( user.roles instanceof Collection );
assert( user.roles.first() instanceof Role );
```
Collection.RefsTo is a collection of models. It overrides toJSON and parse to accept array of model ids. Also, it *override its 'get' property in upper model*, to resolve ids to real models from
the given master collection on first attribute read attempt. If master collection is empty and thus references cannot be resolved, it will defer id resolution and just return collection of dummy models with ids. However, if master
collection is not empty, it will filter out ids of non-existent models.
This semantic is required to deal with collections in asynchronous JS world. Also, there are 'lazy' option for passing reference to master collection:
```javascript
var User = Model.extend({
defaults : {
name : String,
roles : Collection.RefsTo( function(){
return this.collection.rolesCollection; // <- collection of Roles is the direct member of UsersCollection.
})
}
});
```
Note, that 'change' events won't be bubbled from models in Collection.RefsTo. Collection's events will.
Type checks and type coercion rules summary
-------------------------------------------
Type checks and type coercions performed on every model 'set' call and assignment to the model's property. Failed type checks will be logged with console.error as "[Type Error...]..." message.
There are two type cercion rules:
1. When model's attribute has default type, it's may either *hold null or instance of specified type* or its subclass. When it's set with value of different type, constructor will be invoked with this value as an argument to produce specified type.
2. When model's attribute has Model (or Collection) type, it may hold either null or instance of specified Model (Collection) or its subclass. When it's being set with value of different type, it will be either *delegated to 'set' method of existing model/collection* (if current attribute value is not null), or *new model/collection will be created* with the given value as first argument.
And there are two very fast runtime type checks in Model.set, which in combination with coercion rules listed above effectively isolating significant amount of type errors:
1. All model's attributes *must* be declared in 'defaults'. Attempt to set attribute not having default value or type *is treated as an error*.
2. For bulk attributes set operation *only plain JS object may be used* as an argument. Usage of complex objects as attributes hash *is treated as an error*.
Also, there is a protection inside 'extend' from occasionally overriding base Model/Collection/Class members with attributes and properties. Required, since plugin creates native properties for attributes.
Other enhancements
------------------
- isValid method checks nested models, collections, and classes recursively.
- deepClone operation for deep copy of nested models, collections, and types. When you start working with nested stuff seriously, you'll need it soon.

@@ -209,4 +351,5 @@

```
- Class type, which can send and receive Backbone events and can be extended. Also, it can have native properties, as Model and Collection. The basic building block of Backbone, which was not exported from the library directly for some reason.
- Class type, which can send and receive Backbone events and can be extended. Also, it can have native properties, as well as Model and Collection. The basic building block of Backbone, which was not exported from the library directly for some reason.
```javascript

@@ -218,2 +361,6 @@ var myClass = Class.extend({

this.a = options.a
this.listenTo( options.nowhere, 'something', function(){
this.trigger( 'heardsomething', this );
});
},

@@ -226,11 +373,13 @@

```
P.S.: There's an opinion that classes are not needed in JS since you can do fancy mixins with prototypes. What I would say? C'mon, we're really old-school guys here. :) We're get used to situation when class looks like a class, not as a random excerpt from Linux kernel or something :)
Installation and dependencies
-----------------------------
Native properties support is required. It means all modern browsers, IE from version 9.
You need a single file (nestedtypes.js) and backbone.js itself. It should work in browser with plain script tag,
require.js or other AMD loader. Also, it's available as npm package for node.js (https://www.npmjs.org/package/backbone.nested-types).
Module exports three variables - Class, Model, Collection. You need to use them instead of backbone's.
Module exports three variables - Class, Model, Collection. You need to use them instead of backbone's. In browser environment with plain script tag import it will export these things under NestedTypes namespace - see an example in sources.
And yes, you could expect this extension to be stable and bug free enough to use. It is being developed as a part of commercial product. Volicon rely on this extension as a core part of architecture for next gen products. And we're actually interested in your bug reports. :)
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc