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.10.0 to 1.0.0

nestedtypes.min.js

2293

nestedtypes.js

@@ -1,1000 +0,1791 @@

// Backbone.nestedTypes 0.10.0 (https://github.com/Volicon/backbone.nestedTypes)
// (c) 2014 Vlad Balin & Volicon, may be freely distributed under the MIT license
/**
* Backbone.NestedTypes 1.0.0 <https://github.com/Volicon/backbone.nestedTypes>
* (c) 2015 Vlad Balin & Volicon
* Released under MIT @license
*/
// Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601>
// © 2011 Colin Snover <http://zetafleet.com>
// Released under MIT license.
( function( root, factory ){
if( typeof define === 'function' && define.amd ) {
define( [ 'exports', 'backbone', 'underscore' ], factory );
/**
* Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601>
* � 2011 Colin Snover <http://zetafleet.com>
* Released under MIT @license
*/
(function(root, factory) {
if(typeof exports === 'object') {
module.exports = factory(require('underscore'), require('backbone'));
}
else if( typeof exports !== 'undefined' ){
factory( exports, require( 'backbone' ), require( 'underscore' ) );
else if(typeof define === 'function' && define.amd) {
define(['underscore', 'backbone'], factory);
}
else{
root.Nested = root.NestedTypes = {};
factory( root.NestedTypes, root.Backbone, root._ );
else {
root.Nested = factory(root._, root.Backbone);
}
}( this, function( Nested, Backbone, _ ){
Integer = function( x ){ return x ? Math.round( x ) : 0; };
}(this, function( _, Backbone ) {
var require = function(name) {
return { underscore: _, backbone : Backbone }[name];
};
require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
// Options wrapper for chained and safe type specs...
// --------------------------------------------------
require( './object+' );
'use strict';
var extend = Backbone.Model.extend;
var trigger3 = require( './backbone+' ).Events.trigger3,
modelSet = require( './modelset' ),
genericIsChanged = modelSet.isChanged,
setSingleAttr = modelSet.setSingleAttr;
Nested.error = {
propertyConflict : function( context, name ){
console.error( '[Type error](' + context.__class + '.extend) Property ' + name + ' conflicts with base class members' );
},
var primitiveTypes = {
string : String,
number : Number,
boolean : Boolean
};
argumentIsNotAnObject : function( context, value ){
console.error( '[Type Error](' + context.__class + '.set) Attribute hash is not an object:', value, 'In model:', context );
},
// list of simple accessor methods available in options
var availableOptions = [ 'triggerWhenChanged', 'changeEvents', 'parse', 'clone', 'toJSON', 'value', 'cast', 'create', 'name', 'value',
'type' ];
unknownAttribute : function( context, name, value ){
context.suppressTypeErrors || console.error( '[Type Error](' + context.__class + '.set) Attribute "' + name + '" has no default value.', value, 'In model:', context );
var Options = Object.extend( {
_options : {}, // attribute options
Attribute : null, // default attribute spec when no type is given, is set to Attribute below
properties : {
has : function(){ return this; }
},
constructor : function( spec ){
// special option used to guess types of primitive values and to distinguish value from type
if( 'typeOrValue' in spec ){
var typeOrValue = spec.typeOrValue,
primitiveType = primitiveTypes[ typeof typeOrValue ];
if( primitiveType ){
spec = { type : primitiveType, value : typeOrValue };
}
else{
spec = typeof typeOrValue == 'function' ? { type : typeOrValue } : { value : typeOrValue };
}
}
};
function createExtendFor( Base ){
return function( protoProps, staticProps ){
var This = extend.call( this, protoProps, staticProps );
delete This._subsetOf;
this._options = {};
this.options( spec );
},
_.each( protoProps.properties, function( propDesc, name ){
var prop = _.isFunction( propDesc ) ? {
get: propDesc,
enumerable: false
} : propDesc;
// get hooks stored as an array
get : function( getter ){
var options = this._options;
options.get = options.get ? options.get.unshift( getter ) : [ getter ];
return this;
},
if( name in Base.prototype ){
Nested.error.propertyConflict( This.prototype, name );
}
// set hooks stored as an array
set : function( setter ){
var options = this._options;
options.set = options.set ? options.set.push( setter ) : [ setter ];
return this;
},
Object.defineProperty( This.prototype, name, prop );
});
// events must be merged
events : function( events ){
this._options.events = Object.assign( this._options.events || {}, events );
return this;
},
return This;
};
// options must be merged using rules for individual accessors
options : function( options ){
for( var i in options ){
this[ i ]( options[ i ] );
}
return this;
},
// construct attribute with a given name and proper type.
createAttribute : function( name ){
var options = this._options,
Type = options.type ? options.type.Attribute : this.Attribute;
if( options.changeEvents ) options.triggerWhenChanged = options.changeEvents;
return new Type( name, options );
}
} );
var listenTo = Backbone.Model.prototype.listenTo;
availableOptions.forEach( function( name ){
Options.prototype[ name ] = function( value ){
this._options[ name ] = value;
return this;
};
} );
Backbone.Events.listenTo = Backbone.Model.prototype.listenTo =
Backbone.Collection.prototype.listenTo = Backbone.View.prototype.listenTo =
function( source, events ){
if( typeof events === 'object' ){
_.each( events, function( handler, name ){
listenTo.call( this, source, name, handler );
}, this );
}
else{
listenTo.apply( this, arguments );
}
};
function chainHooks( array ){
var l = array.length;
/*************************************************
NestedTypes.Class
- can be extended as native Backbone objects
- can send out and listen to backbone events
- can have native properties
**************************************************/
Nested.Class = ( function(){
function Class(){
this.initialize.apply( this, arguments );
return l === 1 ? array[ 0 ] : function( value, name ){
var res = value;
for( var i = 0; i < l; i++ ){
res = array[ i ].call( this, res, name );
}
return res;
};
}
_.extend( Class.prototype, Backbone.Events, { __class: 'Class', initialize: function (){} } );
Class.extend = createExtendFor( Class );
var transform = {
hookAndCast : function( val, options, model, name ){
var value = this.cast( val, options, model, name ),
prev = model.attributes[ name ];
return Class;
})();
if( this.isChanged( value, prev ) ){
value = this.set.call( model, value, name );
return value === undefined ? prev : this.cast( value, options, model );
}
/*************************************************
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
**************************************************/
return value;
},
function chainHooks( first, second ){
return function( value, name ){
return second.call( this, first.call( this, value, name ), name );
hook : function( value, options, model, name ){
var prev = model.attributes[ name ];
if( this.isChanged( value, prev ) ){
var changed = this.set.call( model, value, name );
return changed === undefined ? prev : changed;
}
return value;
},
delegateAndMore : function( val, options, model, attr ){
return this.delegateEvents( this._transform( val, options, model, attr ), options, model, attr );
}
Nested.options = ( function(){
var Attribute = Nested.Class.extend({
type : null,
};
create : function(){
return new this.type();
},
// Base class for Attribute metatype
// ---------------------------------
property : function( name ){
var spec = {
set : function( value ){
this.set( name, value );
return value;
},
var Attribute = Object.extend( {
name : null,
type : null,
value : undefined,
enumerable : false
},
get = this.get;
// cast function
// may be overriden in subclass
cast : null, // function( value, options, model ),
spec.get = get ? function(){
return get.call( this, this.attributes[ name ], name );
} : function(){
return this.attributes[ name ];
};
// get and set hooks...
get : null,
set : null,
return spec;
},
// user events
events : null, // { event : handler, ... }
options : function( spec ){
if( spec.get && this.get ){
spec.get = chainHooks( this.get, spec.get );
}
if( spec.set && this.set ){
spec.set = chainHooks( this.set, spec.set );
}
_.extend( this, spec );
return this;
},
// system events
__events : null, // { event : handler, ... }
initialize : function( spec ){
this.options( spec );
// create empty object passing backbone options to constructor...
// must be overriden for backbone types only
create : function( options ){ return new this.type(); },
// optimized general purpose isEqual function for typeless attributes
// must be overriden in subclass
isChanged : genericIsChanged,
// generic clone function for typeless attributes
// Must be overriden in sublass
clone : function( value, options ){
if( value && typeof value === 'object' ){
var proto = Object.getPrototypeOf( value );
if( proto.clone ){
// delegate to object's clone if it exist
return value.clone( options );
}
},{
bind : ( function(){
var attributeMethods = {
options : function( spec ){
spec.type || ( spec.type = this );
return new this.NestedType( spec );
},
value : function( value ){
return new this.NestedType({ type : this, value : value });
}
};
if( options && options.deep && proto === Object.prototype || proto === Array.prototype ){
// attempt to deep copy raw objects, assuming they are JSON
return JSON.parse( JSON.stringify( value ) );
}
}
return function(){
_.each( arguments, function( Type ){
_.extend( Type, attributeMethods, { NestedType : this } );
}, this );
};
})()
});
return value;
},
Attribute.extend({
cast : function( value ){
return value == null || value instanceof this.type ? value : new this.type( value );
toJSON : function( value, key ){
return value && value.toJSON ? value.toJSON() : value;
},
// must be overriden for backbone types...
createPropertySpec : function(){
return (function( self, name, get ){
return {
// call to optimized set function for single argument. Doesn't work for backbone types.
set : function( value ){ setSingleAttr( this, name, value, self ); },
// attach get hook to the getter function, if present
get : get ? function(){ return get.call( this, this.attributes[ name ], name ); } :
function(){ return this.attributes[ name ]; }
}
}).bind( Function.prototype );
})( this, this.name, this.get );
},
var primitiveTypes = {
string : String,
number : Number,
boolean : Boolean
};
// automatically generated optimized transform function
// do not touch.
_transform : null,
transform : function( value ){ return value; },
function createAttribute( spec ){
if( arguments.length >= 2 ){
spec = {
type : arguments[ 0 ],
value : arguments[ 1 ]
};
// delegate user and system events on attribute transform
delegateEvents : function( value, options, model, name ){
var prev = model.attributes[ name ];
if( arguments.length >= 3 ){
_.extend( spec, arguments[ 2 ] );
if( this.isChanged( prev, value ) ){ //should be changed only when attr is really replaced.
prev && prev.trigger && model.stopListening( prev );
if( value && value.trigger ){
if( this.events ){
model.listenTo( value, this.events );
}
if( this.__events ){
model.listenTo( value, this.__events );
}
}
else if( 'typeOrValue' in spec ){
var typeOrValue = spec.typeOrValue,
primitiveType = primitiveTypes[ typeof typeOrValue ];
if( primitiveType ){
spec = { type : primitiveType, value : typeOrValue };
trigger3( model, 'replace:' + name, model, value, prev );
}
return value;
},
constructor : function( name, spec ){
this.name = name;
Object.transform( this, spec, function( value, name ){
if( name === 'events' && this.events ){
return Object.assign( this.events, value );
}
if( name === 'get' ){
if( this.get ){
value.unshift( this.get );
}
else{
spec = _.isFunction( typeOrValue ) ? { type : typeOrValue } : { value : typeOrValue };
return chainHooks( value );
}
if( name === 'set' ){
if( this.set ){
value.push( this.set );
}
return chainHooks( value );
}
if( spec.type ){
return spec.type.options( spec );
return value;
}, this );
this.initialize( spec );
// assemble optimized transform function...
if( this.cast ){
this.transform = this._transform = this.cast;
}
if( this.set ){
this.transform = this._transform = this.cast ? transform.hookAndCast : transform.hook;
}
if( this.events || this.__events ){
this.transform =
this._transform ? transform.delegateAndMore : this.delegateEvents;
}
}
}, {
attach : (function(){
function options( spec ){
spec || ( spec = {} );
spec.type || ( spec.type = this );
return new Options( spec );
}
function value( value ){
return new Options( { type : this, value : value } );
}
return function(){
for( var i = 0; i < arguments.length; i++ ){
var Type = arguments[ i ];
Type.attribute = Type.options = options;
Type.value = value;
Type.Attribute = this;
Object.defineProperty( Type, 'has', {
get : function(){
// workaround for sinon.js and other libraries overriding 'has'
return this._has || this.options();
},
set : function( value ){ this._has = value; }
} );
}
else{
return new Attribute( spec );
}
};
})()
} );
Options.prototype.Attribute = Attribute;
Options.prototype.attribute = Options.prototype.options;
function createOptions( spec ){
return new Options( spec );
}
createOptions.Type = Attribute;
createOptions.create = function( options, name ){
if( !( options && options instanceof Options ) ){
options = new Options( { typeOrValue : options } );
}
return options.createAttribute( name );
};
module.exports = createOptions;
},{"./backbone+":2,"./modelset":7,"./object+":8}],2:[function(require,module,exports){
/* Backbone core extensions: bug fixes and optimizations
- Use Object+ for all backbone objects
- Fix for Events.listenTo to support message maps
- optimized trigger functions
* (c) Vlad Balin & Volicon, 2015
* ------------------------------------------------------------- */
var Class = require( './object+' ),
Backbone = require( 'backbone' );
module.exports = Backbone;
// Workaround for backbone 1.2.0 listenTo event maps bug
var Events = Backbone.Events,
bbListenTo = Events.listenTo;
Events.listenTo = function( obj, events ){
if( typeof events === 'object' ){
for( var event in events ) bbListenTo.call( this, obj, event, events[ event ] );
return this;
}
return bbListenTo.apply( this, arguments );
};
// Update Backbone objects to use event patches and Object+
[ 'Model', 'Collection', 'View', 'Router', 'History' ].forEach( function( name ){
var Type = Backbone[ name ];
Type.prototype.listenTo = Events.listenTo;
Object.extend.attach( Type );
});
// Make Object.extend classes capable of sending and receiving Backbone Events...
Object.assign( Class.prototype, Events );
// So hard to believe :) You won't. Optimized JIT-friendly event trigger functions to be used from model.set
// Two specialized functions for event triggering...
Events.trigger2 = function( self, name, a, b ){
var _events = self._events;
if( _events ){
_fireEvent2( _events[ name ], a, b );
_fireEvent3( _events.all, name, a, b );
}
};
Events.trigger3 = function( self, name, a, b, c ){
var _events = self._events;
if( _events ){
_fireEvent3( _events[ name ], a, b, c );
_fireEvent4( _events.all, name, a, b, c );
}
};
// ...and specialized functions with triggering loops. Crappy JS JIT loves these small functions and code duplication.
function _fireEvent2( events, a, b ){
if( events )
for( var i = 0, l = events.length, ev; i < l; i ++ )
(ev = events[i]).callback.call(ev.ctx, a, b);
}
function _fireEvent3( events, a, b, c ){
if( events )
for( var i = 0, l = events.length, ev; i < l; i ++ )
(ev = events[i]).callback.call(ev.ctx, a, b, c);
}
function _fireEvent4( events, a, b, c, d ){
if( events )
for( var i = 0, l = events.length, ev; i < l; i ++ )
(ev = events[i]).callback.call(ev.ctx, a, b, c, d);
}
},{"./object+":8,"backbone":"backbone"}],3:[function(require,module,exports){
var Backbone = require( './backbone+' ),
Model = require( './model' ),
error = require( './errors' ),
_ = require( 'underscore' );
var CollectionProto = Backbone.Collection.prototype;
function wrapCall( func ){
return function(){
if( !this.__changing++ ){
this.trigger( 'before:change' );
}
createAttribute.Type = Attribute;
return createAttribute;
})();
var res = func.apply( this, arguments );
Nested.defaults = function( x ){
return Nested.Model.defaults( x );
if( !--this.__changing ){
this.trigger( 'after:change' );
}
return res;
};
Nested.value = function( value ){ return Nested.options({ value: value }); };
}
( function(){
var numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ],
msDatePattern = /\/Date\(([0-9]+)\)\//,
isoDatePattern = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/;
module.exports = Backbone.Collection.extend( {
triggerWhenChanged : Backbone.VERSION >= '1.2.0' ? 'update change reset' : 'add remove change reset',
__class : 'Collection',
function parseDate( date ){
var msDate, timestamp, struct, minutesOffset = 0;
model : Model,
if( msDate = msDatePattern.exec( date ) ){
timestamp = Number( msDate[ 1 ] );
}
else if(( struct = isoDatePattern.exec( date ))) {
// avoid NaN timestamps caused by “undefined” values being passed to Date.UTC
for( var i = 0, k; ( k = numericKeys[i] ); ++i ) {
struct[ k ] = +struct[ k ] || 0;
}
isValid : function( options ){
return this.every( function( model ){
return model.isValid( options );
} );
},
// allow undefined days and months
struct[ 2 ] = (+struct[ 2 ] || 1) - 1;
struct[ 3 ] = +struct[ 3 ] || 1;
get : function( obj ){
if( obj == null ){
return void 0;
}
return typeof obj === 'object' ? this._byId[ obj.id ] || this._byId[ obj.cid ] : this._byId[ obj ];
},
if (struct[ 8 ] !== 'Z' && struct[ 9 ] !== undefined) {
minutesOffset = struct[ 10 ] * 60 + struct[ 11 ];
deepClone : function(){ return this.clone( { deep : true } ); },
if (struct[ 9 ] === '+') {
minutesOffset = 0 - minutesOffset;
}
}
clone : function( options ){
var models = options && options.deep ?
this.map( function( model ){
return model.clone( options );
} ) : this.models;
timestamp = Date.UTC(struct[ 1 ], struct[ 2 ], struct[ 3 ], struct[ 4 ], struct[ 5 ] + minutesOffset, struct[ 6 ], struct[ 7 ]);
return new this.constructor( models );
},
__changing : 0,
set : wrapCall( function( models, options ){
if( models ){
if( typeof models !== 'object' || !( models instanceof Array || models instanceof Model ||
Object.getPrototypeOf( models ) === Object.prototype ) ){
error.wrongCollectionSetArg( this, models );
}
else {
timestamp = Date.parse( date );
}
return timestamp;
}
Nested.options.Type.extend({
cast : function( value ){
if( value == null || value instanceof Date ){
return value;
}
return CollectionProto.set.call( this, models, options );
} ),
if( _.isString( value ) ){
value = parseDate( value );
}
remove : wrapCall( CollectionProto.remove ),
add : wrapCall( CollectionProto.add ),
reset : wrapCall( CollectionProto.reset ),
sort : wrapCall( CollectionProto.sort ),
return new Date( value );
}
}).bind( Date );
})();
getModelIds : function(){ return _.pluck( this.models, 'id' ); }
}, {
// Cache for subsetOf collection subclass.
__subsetOf : null,
defaults : function( attrs ){
return this.prototype.model.extend( { defaults : attrs } ).Collection;
},
extend : function(){
// Need to subsetOf cache when extending the collection
var This = Backbone.Collection.extend.apply( this, arguments );
This.__subsetOf = null;
return This;
}
} );
// Fix incompatible constructor behaviour of primitive types...
Nested.options.Type.extend({
create : function(){
return this.type();
},
},{"./backbone+":2,"./errors":4,"./model":6,"underscore":"underscore"}],4:[function(require,module,exports){
require( './object+' );
cast : function( value ){
return value == null ? null : this.type( value );
function format( value ){
return typeof value === 'string' ? '"' + value + '"' : value;
}
Object.assign( Object.extend.error, {
argumentIsNotAnObject : function( context, value ){
//throw new TypeError( 'Attribute hash is not an object in ' + context.__class + '.set(', value, ')' );
console.error( '[Type Error] Attribute hash is not an object in ' +
context.__class + '.set(', format( value ), '); this =', context );
},
unknownAttribute : function( context, name, value ){
if( context.suppressTypeErrors ) return;
console.warn( '[Type Error] Attribute has no default value in ' +
context.__class + '.set( "' + name + '",', format( value ), '); this =', context );
},
wrongCollectionSetArg : function( context, value ){
//throw new TypeError( 'Wrong argument type in ' + context.__class + '.set(' + value + ')' );
console.error( '[Type Error] Wrong argument type in ' +
context.__class + '.set(', format( value ), '); this =', context );
}
});
module.exports = Object.extend.error;
},{"./object+":8}],5:[function(require,module,exports){
// Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601>
// © 2011 Colin Snover <http://zetafleet.com>
// Released under MIT license.
// Attribute Type definitions for core JS types
// ============================================
var attribute = require( './attribute' ),
modelSet = require( './modelset' ),
Model = require( './model' ),
Collection = require( './collection' );
// Constructors Attribute
// ----------------
attribute.Type.extend( {
cast : function( value ){
return value == null || value instanceof this.type ? value : new this.type( value );
},
clone : function( value, options ){
// delegate to clone function or deep clone through serialization
return value.clone ? value.clone( value, options ) : this.cast( JSON.parse( JSON.stringify( value ) ) );
}
} ).attach( Function.prototype );
// Date Attribute
// ----------------------
var numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ],
msDatePattern = /\/Date\(([0-9]+)\)\//,
isoDatePattern = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/;
function parseDate( date ){
var msDate, timestamp, struct, minutesOffset = 0;
if( msDate = msDatePattern.exec( date ) ){
timestamp = Number( msDate[ 1 ] );
}
else if( ( struct = isoDatePattern.exec( date )) ){
// avoid NaN timestamps caused by �undefined� values being passed to Date.UTC
for( var i = 0, k; ( k = numericKeys[ i ] ); ++i ){
struct[ k ] = +struct[ k ] || 0;
}
}).bind( Number, Boolean, String, Integer );
// Fix incompatible constructor behaviour of Array...
Nested.options.Type.extend({
cast : function( value ){
return value == null || value instanceof Array ? value : [ value ];
// allow undefined days and months
struct[ 2 ] = (+struct[ 2 ] || 1) - 1;
struct[ 3 ] = +struct[ 3 ] || 1;
if( struct[ 8 ] !== 'Z' && struct[ 9 ] !== undefined ){
minutesOffset = struct[ 10 ] * 60 + struct[ 11 ];
if( struct[ 9 ] === '+' ){
minutesOffset = 0 - minutesOffset;
}
}
}).bind( Array );
var baseModelSet = Backbone.Model.prototype.set;
timestamp =
Date.UTC( struct[ 1 ], struct[ 2 ], struct[ 3 ], struct[ 4 ], struct[ 5 ] + minutesOffset, struct[ 6 ],
struct[ 7 ] );
}
else{
timestamp = Date.parse( date );
}
Nested.Model = ( function(){
var ModelProto = Backbone.Model.prototype;
return timestamp;
}
var Model = Backbone.Model.extend( {
triggerWhenChanged: 'change',
listening: {},
attribute.Type.extend( {
cast : function( value ){
return value == null || value instanceof Date ? value :
new Date( typeof value === 'string' ? parseDate( value ) : value )
},
__defaults: {},
__attributes: { id : Nested.options({ name: 'id', value : undefined }) },
__class : 'Model',
toJSON : function( value ){ return value && value.toJSON(); },
__duringSet: 0,
isChanged : function( a, b ){ return ( a && +a ) !== ( b && +b ); },
clone : function( value ){ return new Date( +value ); }
} ).attach( Date );
__beginChange : function(){
this.__duringSet++ || ( this.__nestedChanges = {} );
},
// Primitive Types
// ----------------
// Global Mock for missing Integer data type...
// -------------------------------------
Integer = function( x ){ return x ? Math.round( x ) : 0; };
__commitChange : function( attrs, options ){
if( !--this.__duringSet ){
attrs || ( attrs = {} );
attribute.Type.extend( {
create : function(){ return this.type(); },
for( var name in this.__nestedChanges ){
name in attrs || ( attrs[ name ] = this.__nestedChanges[ name ] );
toJSON : function( value ){ return value; },
cast : function( value ){ return value == null ? null : this.type( value ); },
if( attrs[ name ] === this.attributes[ name ] ){
this.attributes[ name ] = null;
}
}
isChanged : function( a, b ){ return a !== b; },
this.__nestedChanges = {};
}
clone : function( value ){ return value; }
} ).attach( Number, Boolean, String, Integer );
attrs && baseModelSet.call( this, attrs, options );
},
// Array Type
// ---------------
attribute.Type.extend( {
toJSON : function( value ){ return value; },
cast : function( value ){
// Fix incompatible constructor behaviour of Array...
return value == null || value instanceof Array ? value : [ value ];
}
} ).attach( Array );
_bulkSet : function( attrs, options ){
if( Object.getPrototypeOf( attrs ) !== Object.prototype ){
Nested.error.argumentIsNotAnObject( this, attrs );
}
// Backbone Attribute
// ----------------
var attrSpecs = this.__attributes;
this.__beginChange();
// helper attrSpec mock to force attribute update
var bbForceUpdateAttr = new ( attribute.Type.extend( {
isChanged : function(){ return true; }
} ) );
for( var name in attrs ){
var attrSpec = attrSpecs[ name ],
value = attrs[ name ];
var setAttrs = modelSet.setAttrs,
setSingleAttr = modelSet.setSingleAttr;
if( attrSpec ){
attrSpec.cast && ( value = attrSpec.cast( value, options, this ) );
attribute.Type.extend( {
create : function( options ){ return new this.type( null, options ); },
clone : function( value, options ){ return value && value.clone( options ); },
toJSON : function( value ){ return value && value.toJSON(); },
if( attrSpec.set && value !== this.attributes[ name ] ){
value = attrSpec.set.call( this, value, name );
if( value === undefined ){
delete attrs[ name ];
continue;
}
attrSpec.cast && ( value = attrSpec.cast( value, options, this ) );
}
isChanged : function( a, b ){ return a !== b; },
attrSpec.delegateEvents && attrSpec.delegateEvents( this, this.attributes[ name ], value );
isBackboneType : true,
isModel : true,
createPropertySpec : function(){
// if there are nested changes detection enabled, disable optimized setter
if( this.__events ){
return (function( self, name, get ){
return {
set : function( value ){
var attrs = {};
attrs[ name ] = value;
}
else{
Nested.error.unknownAttribute( this, name, value );
}
setAttrs( this, attrs );
},
get : get ? function(){ return get.call( this, this.attributes[ name ], name ); } :
function(){ return this.attributes[ name ]; }
}
})( this, this.name, this.get );
}
else{
return attribute.Type.prototype.createPropertySpec.call( this );
}
},
this.__commitChange( attrs, options );
return this;
},
cast : function( value, options, model, name ){
var incompatibleType = value != null && !( value instanceof this.type ),
existingModelOrCollection = model.attributes[ name ];
set : function( name, value, options ){
if( typeof name === 'object' ){
return this._bulkSet( name, value );
if( incompatibleType ){
if( existingModelOrCollection ){ // ...delegate update for existing object 'set' method
if( options && options.parse && this.isModel ){ // handle inconsistent backbone's parse implementation
value = existingModelOrCollection.parse( value );
}
var attrSpec = this.__attributes[ name ];
existingModelOrCollection.set( value, options );
value = existingModelOrCollection;
}
else{ // ...or create a new object, if it's not exist
value = new this.type( value, options );
}
}
if( attrSpec ){
if( attrSpec.isBackboneType ){
var attrs = {};
attrs[ name ] = value;
return this._bulkSet( attrs, options );
}
return value;
},
attrSpec.cast && ( value = attrSpec.cast( value, options, this ) );
initialize : function( spec ){
var name = this.name,
triggerWhenChanged = this.triggerWhenChanged || spec.type.prototype.triggerWhenChanged;
if( attrSpec.set && value !== this.attributes[ name ] ){
value = attrSpec.set.call( this, value, name );
if( value === undefined ) return this;
attrSpec.cast && ( value = attrSpec.cast( value, options, this ) );
}
this.isModel = this.type.prototype instanceof Model;
if( triggerWhenChanged ){
// for collection, add transactional methods to join change events on bubbling
this.__events = this.isModel ? {} : {
'before:change' : modelSet.__begin,
'after:change' : modelSet.__commit
};
this.__events[ triggerWhenChanged ] = function handleNestedChange(){
var attr = this.attributes[ name ];
if( this.__duringSet ){
this.__nestedChanges[ name ] = attr;
}
else{
Nested.error.unknownAttribute( this, name, value );
setSingleAttr( this, name, attr, bbForceUpdateAttr );
}
};
}
}
} ).attach( Model, Collection );
return baseModelSet.call( this, name, value, options );
},
},{"./attribute":1,"./collection":3,"./model":6,"./modelset":7}],6:[function(require,module,exports){
var BaseModel = require( './backbone+' ).Model,
modelSet = require( './modelset' ),
attrOptions = require( './attribute' ),
error = require( './errors' ),
_ = require( 'underscore' ),
ModelProto = BaseModel.prototype;
deepGet : function( name ){
var path = name.split( '.' ),
l = path.length,
value = this;
var setSingleAttr = modelSet.setSingleAttr,
setAttrs = modelSet.setAttrs,
applyTransform = modelSet.transform;
for( var i = 0; i < l; i++ ){
value = value.get( path[ i ] );
}
function cloneAttrs( attrSpecs, attrs, options ){
for( var name in attrs ){
attrs[ name ] = attrSpecs[ name ].clone( attrs[ name ], options );
}
return value;
},
return attrs;
}
deepSet : function( name, value, options ){
var path = name.split( '.' ),
l = path.length - 1,
model = this,
attr = path[ l ];
var Model = BaseModel.extend( {
triggerWhenChanged : 'change',
for( var i = 0; i < l; i++ ){
model = model.get( path[ i ] );
}
properties : {
id : {
get : function(){
var name = this.idAttribute;
return model.set( attr, value, options );
// TODO: get hook doesn't work for idAttribute === 'id'
return name === 'id' ? this.attributes.id : this[ name ];
},
constructor : function(attributes, options){
var attrs = attributes || {};
options || (options = {});
this.cid = _.uniqueId( 'c' );
this.attributes = {};
if( options.collection ) this.collection = options.collection;
if( options.parse ) attrs = this.parse( attrs, options ) || {};
attrs = _.defaults( {}, attrs, this.defaults( options ) );
this.set( attrs, options );
this.changed = {};
this.initialize.apply( this, arguments );
},
// override get to invoke native getter...
get : function( name ){ return this[ name ]; },
set : function( value ){
var name = this.idAttribute;
setSingleAttr( this, name, value, this.__attributes[ name ] );
}
}
},
// Create deep copy for all nested objects...
deepClone: function( options ){
var attrs = {};
__attributes : { id : attrOptions( { value : undefined } ).createAttribute( 'id' ) },
__class : 'Model',
_.each( this.attributes, function( value, key ){
attrs[ key ] = value && value.deepClone ? value.deepClone( options ) : value;
});
__duringSet : 0,
return new this.constructor( attrs, options );
},
defaults : function(){ return {}; },
// Support for nested models and objects.
// Apply toJSON recursively to produce correct JSON.
toJSON: function(){
var res = {};
__begin : modelSet.__begin,
__commit : modelSet.__commit,
_.each( this.attributes, function( value, key ){
var spec = this.__attributes[ key ],
toJSON = spec && spec.toJSON;
set : function( a, b, c ){
switch( typeof a ){
case 'string' :
var attrSpec = this.__attributes[ a ];
if( toJSON !== false ){
if( _.isFunction( toJSON ) ){
res[ key ] = toJSON.call( this, value, key );
if( attrSpec && !attrSpec.isBackboneType && !c ){
return setSingleAttr( this, a, b, attrSpec );
}
var attrs = {};
attrs[ a ] = b;
return setAttrs( this, attrs, c );
case 'object' :
if( a && Object.getPrototypeOf( a ) === Object.prototype ){
return setAttrs( this, a, b );
}
default :
error.argumentIsNotAnObject( this, a );
}
},
// Return model's value for dot-separated 'deep reference'.
// Model id and cid are allowed for collection elements.
// If path is not exist, 'undefined' is returned.
// model.deepGet( 'a.b.c123.x' )
deepGet : function( name ){
var path = name.split( '.' ), value = this;
for( var i = 0, l = path.length; value && i < l; i++ ){
value = value.get ? value.get( path[ i ] ) : value[ path[ i ] ];
}
return value;
},
// Set model's value for dot separated 'deep reference'.
// If model doesn't exist at some path, create default models
// if options.nullify is given, assign attributes with nulls
deepSet : function( name, value, options ){
var path = name.split( '.' ),
l = path.length - 1,
model = this,
attr = path[ l ];
for( var i = 0; i < l; i++ ){
var current = path[ i ],
next = model.get ? model.get( current ) : model[ current ];
// Create models in path, if they are not exist.
if( !next ){
var attrSpecs = model.__attributes;
if( attrSpecs ){
// If current object is model, create default attribute
var newModel = attrSpecs[ current ].create( options );
// If created object is model, nullify attributes when requested
if( options && options.nullify && newModel.__attributes ){
var nulls = new newModel.Attributes( {} );
for( var key in nulls ){
nulls[ key ] = null;
}
else{
res[ key ] = value && value.toJSON ? value.toJSON() : value;
}
newModel.set( nulls );
}
}, this );
return res;
},
model[ current ] = next = newModel;
}
else{
return;
} // silently fail in other case
}
model = next;
}
parse : function( data ){
var attrs = {},
parsed = false;
return model.set ? model.set( attr, value, options ) : model[ attr ] = value;
},
_.each( data, function( value, name ){
var spec = this.__attributes[ name ];
if( spec && spec.parse ){
parsed = true;
attrs[ name ] = spec.parse.call( this, value, name );
}
}, this );
constructor : function( attributes, opts ){
var attrSpecs = this.__attributes,
attrs = attributes || {},
options = opts || {};
return parsed ? _.defaults( attrs, data ) : data;
},
this.cid = _.uniqueId( 'c' );
this.attributes = {};
if( options.collection ){
this.collection = options.collection;
}
if( options.parse ){
attrs = this.parse( attrs, options ) || {};
}
isValid : function( options ){
return ModelProto.isValid.call( this, options ) && _.every( this.attributes, function( attr ){
if( attr && attr.isValid ){
return attr.isValid( options );
}
else if( attr instanceof Date ){
return !_.isNaN( attr.getTime() );
}
else{
return !_.isNaN( attr );
}
});
},
if( typeof attrs !== 'object' || Object.getPrototypeOf( attrs ) !== Object.prototype ){
error.argumentIsNotAnObject( this, attrs );
attrs = {};
}
_: _ // add underscore to be accessible in templates
} );
attrs = options.deep ?
cloneAttrs( attrSpecs, new this.Attributes( attrs ), options ) :
this.defaults( attrs, options );
function parseDefaults( spec, Base ){
var defaultAttrs = _.isFunction( spec.defaults ) ? spec.defaults() : spec.defaults || spec.attributes || {},
defaults = _.defaults( defaultAttrs, Base.prototype.__defaults ),
idAttrName = spec.idAttribute || Base.prototype.idAttribute,
attributes = {};
// Execute attributes transform function instead of this.set
applyTransform( this, attrs, attrSpecs, options );
_.each( defaults, function( attr, name ){
attr instanceof Nested.options.Type || ( attr = Nested.options({ typeOrValue: attr }) );
attr.name = name;
this.attributes = attrs;
this.changed = {};
this.initialize.apply( this, arguments );
},
// override get to invoke native getter...
get : function( name ){ return this[ name ]; },
name in defaultAttrs || ( attr.property = false );
// override clone to pass options to constructor
clone : function( options ){
return new this.constructor( this.attributes, options );
},
attributes[ name ] = attr;
});
// Create deep copy for all nested objects...
deepClone : function(){ return this.clone( { deep : true } ); },
// Handle id attribute, whenever it was defined or not...
var idAttr = attributes[ idAttrName ] || ( attributes[ idAttrName ] = Nested.options({ value : undefined }) );
'value' in idAttr || ( idAttr.value = undefined ); // id attribute must have no default value
idAttr.name = idAttrName;
// Support for nested models and objects.
// Apply toJSON recursively to produce correct JSON.
toJSON : function(){
var res = {},
attrs = this.attributes, attrSpecs = this.__attributes;
if( idAttrName === 'id' ){
idAttr.property = false; // to prevent conflict with backbone's model 'id'
for( var key in attrs ){
var value = attrs[ key ], attrSpec = attrSpecs[ key ],
toJSON = attrSpec && attrSpec.toJSON;
if( toJSON ){
res[ key ] = toJSON.call( this, value, key );
}
}
return _.extend( _.omit( spec, 'collection', 'attributes' ), {
__defaults : defaults, // needed for attributes inheritance
__attributes : attributes,
defaults : _.isFunction( spec.defaults ) ? spec.defaults : createDefaults( attributes )
});
return res;
},
parse : function( resp ){ return this._parse( resp ); },
_parse : _.identity,
isValid : function( options ){
// todo: need to do something smart with validation logic
// something declarative on attributes level, may be
return ModelProto.isValid.call( this, options ) && _.every( this.attributes, function( attr ){
if( attr && attr.isValid ){
return attr.isValid( options );
}
return attr instanceof Date ? !_.isNaN( attr.getTime() ) : !_.isNaN( attr );
} );
},
_ : _ // add underscore to be accessible in templates
}, {
// shorthand for inline nested model definitions
defaults : function( attrs ){ return this.extend( { defaults : attrs } ); },
// extend Model and its Collection
extend : function( protoProps, staticProps ){
var This = Object.extend.call( this );
This.Collection = this.Collection.extend();
return protoProps ? This.define( protoProps, staticProps ) : This;
},
// define Model and its Collection. All the magic starts here.
define : function( protoProps, staticProps ){
var Base = Object.getPrototypeOf( this.prototype ).constructor,
spec = createDefinition( protoProps, Base ),
This = this;
Object.extend.Class.define.call( This, spec, staticProps );
// define Collection
var collectionSpec = { model : This };
spec.urlRoot && ( collectionSpec.url = spec.urlRoot );
This.Collection.define( _.defaults( protoProps.collection || {}, collectionSpec ) );
return This;
}
} );
// Create model definition from protoProps spec.
function createDefinition( protoProps, Base ){
var defaults = protoProps.defaults || protoProps.attributes || {},
defaultsAsFunction = typeof defaults == 'function' && defaults,
baseAttrSpecs = Base.prototype.__attributes;
// Support for legacy backbone defaults as functions.
if( defaultsAsFunction ){
defaults = defaults();
}
var attrSpecs = Object.transform( {}, defaults, attrOptions.create );
// Create attribute for idAttribute, if it's not declared explicitly
var idAttribute = protoProps.idAttribute;
if( idAttribute && !attrSpecs[ idAttribute ] ){
attrSpecs[ idAttribute ] = attrOptions( { value : undefined } ).createAttribute( idAttribute );
}
// Prevent conflict with backbone model's 'id' property
if( attrSpecs[ 'id' ] ){
attrSpecs[ 'id' ].createPropertySpec = false;
}
var allAttrSpecs = _.defaults( {}, attrSpecs, baseAttrSpecs ),
Attributes = createCloneCtor( allAttrSpecs );
return _.extend( _.omit( protoProps, 'collection', 'attributes' ), {
__attributes : new Attributes( allAttrSpecs ),
_parse : create_parse( allAttrSpecs, attrSpecs ) || Base.prototype._parse,
defaults : defaultsAsFunction || createDefaults( allAttrSpecs ),
properties : createAttrsNativeProps( protoProps.properties, attrSpecs ),
Attributes : Attributes
} );
}
// Create attributes 'parse' option function only if local 'parse' options present.
// Otherwise return null.
function create_parse( allAttrSpecs, attrSpecs ){
var statements = [ 'var a = this.__attributes;' ],
create = false;
for( var name in allAttrSpecs ){
// Is there any 'parse' option in local model definition?
if( attrSpecs[ name ] && attrSpecs[ name ].parse ) create = true;
// Add statement for each attribute with 'parse' option.
if( allAttrSpecs[ name ].parse ){
var s = 'if("' + name + '" in r) r.' + name + '=a.' + name + '.parse.call(this,r.' + name + ',"' + name + '");';
statements.push( s );
}
}
function isJsonLiteral( value ){
var type = typeof value,
isJSON = value === null || type === 'number' || type === 'string' || type === 'boolean';
statements.push( 'return r;' );
if( !isJSON && type === 'object' ){
var proto = Object.getPrototypeOf( value );
return create ? new Function( 'r', statements.join( '' ) ) : null;
}
if( proto === Object.prototype || proto === Array.prototype ){
isJSON = _.every( value, isJsonLiteral );
}
// Create constructor for efficient attributes clone operation.
function createCloneCtor( attrSpecs ){
var statements = [];
for( var name in attrSpecs ){
statements.push( "this." + name + "=x." + name + ";" );
}
var Attributes = new Function( "x", statements.join( '' ) );
// attributes hash must look like vanilla object, otherwise Model.set will trigger an exception
Attributes.prototype = Object.prototype;
return Attributes;
}
// Check if value is valid JSON.
function isValidJSON( value ){
if( value === null ){
return true;
}
switch( typeof value ){
case 'number' :
case 'string' :
case 'boolean' :
return true;
case 'object':
var proto = Object.getPrototypeOf( value );
if( proto === Object.prototype || proto === Array.prototype ){
return _.every( value, isValidJSON );
}
}
return false;
}
// Create optimized model.defaults( attrs, options ) function
function createDefaults( attrSpecs ){
var statements = [], init = {}, refs = {};
// Compile optimized constructor function for efficient deep copy of JSON literals in defaults.
_.each( attrSpecs, function( attrSpec, name ){
if( attrSpec.value === undefined && attrSpec.type ){
// if type with no value is given, create an empty object
init[ name ] = attrSpec;
statements.push( 'this.' + name + '=i.' + name + '.create( o );' );
}
else{
// If value is given, type casting logic will do the job later, converting value to the proper type.
if( isValidJSON( attrSpec.value ) ){
// JSON literals must be deep copied.
statements.push( 'this.' + name + '=' + JSON.stringify( attrSpec.value ) + ';' );
}
else if( attrSpec.value === undefined ){
// handle undefined value separately. Usual case for model ids.
statements.push( 'this.' + name + '=undefined;' );
}
else{
// otherwise, copy value by reference.
refs[ name ] = attrSpec.value;
statements.push( 'this.' + name + '=r.' + name + ';' );
}
return isJSON;
}
} );
function createDefaults( attributes ){
var json = [], init = {}, refs = {};
var Defaults = new Function( 'r', 'i', 'o', statements.join( '' ) );
Defaults.prototype = Object.prototype;
_.each( attributes, function( attr, name ){
if( attr.value !== undefined ){
if( isJsonLiteral( attr.value ) ){
json.push( name + ':' + JSON.stringify( attr.value ) ); // and make a deep copy
}
else{ // otherwise, copy it by reference.
refs[ name ] = attr.value;
}
// Create model.defaults( attrs, options ) function
// 'attrs' will override default values, options will be passed to nested backbone types
return function( attrs, options ){
var opts = options, name;
// 'collection' and 'parse' options must not be passed down to default nested models and collections
if( options && ( options.collection || options.parse ) ){
opts = {};
for( name in options ){
if( name !== 'collection' && name !== 'parse' ){
opts[ name ] = options[ name ];
}
else{
attr.type && ( init[ name ] = attr );
}
});
}
}
var literals = new Function( 'return {' + json.join( ',' ) + '}' );
var defaults = new Defaults( refs, init, opts );
return function( options ){
if( options && ( options.collection || options.parse ) ){
options = _.omit( options, 'collection', 'parse' );
}
// assign attrs, overriding defaults
for( var name in attrs ){
defaults[ name ] = attrs[ name ];
}
var defaults = literals();
return defaults;
}
}
_.extend( defaults, refs );
// Create native properties for model's attributes
function createAttrsNativeProps( properties, attrSpecs ){
if( properties === false ){
return {};
}
for( var name in init ){
defaults[ name ] = init[ name ].create( null, options );
}
properties || ( properties = {} );
return defaults;
}
return Object.transform( properties, attrSpecs, function( attrSpec, name ){
if( !properties[ name ] && attrSpec.createPropertySpec ){
return attrSpec.createPropertySpec();
}
} );
}
function createNativeProperties( This, spec ){
var properties = {};
module.exports = Model;
if( spec.properties !== false ){
_.each( spec.__attributes, function( attr, name ){
attr.property && ( properties[ name ] = attr.property( name ) );
} );
},{"./attribute":1,"./backbone+":2,"./errors":4,"./modelset":7,"underscore":"underscore"}],7:[function(require,module,exports){
// Optimized Model.set functions
//---------------------------------
/*
Does two main things:
1) Invoke model-specific constructor for attributes cloning. It improves performance on large model updates.
2) Invoke attribute-specific comparison function. Improves performance for everything, especially nested stuff.
_.each( spec.properties, function( propDesc, name ){
properties[ name ] = _.isFunction( propDesc ) ? {
get: propDesc,
enumerable: false
} : _.defaults( {}, propDesc, { enumerable : false } );
});
attrSpec is required to provide two methods:
transform( value, options, model, name ) -> value
to transform value before assignment
_.each( properties, function( prop, name ){
if( name in ModelProto ||
name === 'cid' || name === 'id' || name === 'attributes' ){
Nested.error.propertyConflict( This.prototype, name );
}
isChanged( value1, value2 ) -> bool
to detect whenever attribute must be assigned and counted as changed
Object.defineProperty( This.prototype, name, prop );
});
}
Model is required to implement Attributes constructor for attributes cloning.
*/
// Special case set: used from model's native properties.
// Single attribute change, no options, _no_ _nested_ _changes_ detection on deep update.
// 1) Code is stripped for this special case
// 2) attribute-specific transform function invoked internally
var _ = require( 'underscore' ),
Events = require( './backbone+' ).Events,
error = require( './errors' ),
trigger2 = Events.trigger2,
trigger3 = Events.trigger3;
module.exports = {
isChanged : genericIsChanged,
setSingleAttr : setSingleAttr,
setAttrs : setAttrs,
transform : applyTransform,
__begin : __begin,
__commit : __commit
};
function genericIsChanged( a, b ){
return !( a === b || ( a && b && typeof a == 'object' && typeof b == 'object' && _.isEqual( a, b ) ) );
}
function setSingleAttr( model, key, value, attrSpec ){
'use strict';
var changing = model._changing,
current = model.attributes;
model._changing = true;
if( !changing ){
model._previousAttributes = new model.Attributes( current );
model.changed = {};
}
var prev = model._previousAttributes,
options = {},
val = attrSpec.transform( value, options, model, key ),
isChanged = attrSpec.isChanged;
isChanged( prev[ key ], val ) ? model.changed[ key ] = val : delete model.changed[ key ];
if( isChanged( current[ key ], val ) ){
current[ key ] = val;
model._pending = options;
trigger3( model, 'change:' + key, model, val, options );
}
if( changing ){
return model;
}
while( model._pending ){
options = model._pending;
model._pending = false;
trigger2( model, 'change', model, options );
}
model._pending = false;
model._changing = false;
return model;
}
// General case set: used for multiple and nested model/collection attributes.
// Does _not_ invoke attribute transform! It must be done at the the top level,
// due to the problems with current nested changes detection algorithm. See 'setAttrs' function below.
function bbSetAttrs( model, attrs, opts ){
'use strict';
var options = opts || {};
// Run validation.
if( !model._validate( attrs, options ) ){
return false;
}
// Extract attributes and options.
var unset = options.unset,
silent = options.silent,
changes = [],
changing = model._changing,
current = model.attributes,
attrSpecs = model.__attributes;
model._changing = true;
if( !changing ){
model._previousAttributes = new model.Attributes( current );
model.changed = {};
}
var prev = model._previousAttributes;
// For each `set` attribute, update or delete the current value.
for( var attr in attrs ){
var attrSpec = attrSpecs[ attr ],
isChanged = attrSpec ? attrSpec.isChanged : genericIsChanged,
val = attrs[ attr ];
if( isChanged( current[ attr ], val ) ){
changes.push( attr );
}
Model.extend = function( protoProps, staticProps ){
var spec = parseDefaults( protoProps, this );
var This = extend.call( this, spec, staticProps );
if( isChanged( prev[ attr ], val ) ){
model.changed[ attr ] = val;
}
else{
delete model.changed[ attr ];
}
var collectionSpec = { model : This };
spec.urlRoot && ( collectionSpec.url = spec.urlRoot );
This.Collection = this.Collection.extend( _.defaults( protoProps.collection || {}, collectionSpec ));
unset ? delete current[ attr ] : current[ attr ] = val;
}
createNativeProperties( This, spec );
// Trigger all relevant attribute changes.
if( !silent ){
if( changes.length ){
model._pending = options;
}
for( var i = 0, l = changes.length; i < l; i++ ){
attr = changes[ i ];
trigger3( model, 'change:' + attr, model, current[ attr ], options );
}
}
return This;
};
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if( changing ){
return model;
}
if( !silent ){
while( model._pending ){
options = model._pending;
model._pending = false;
trigger2( model, 'change', model, options );
}
}
Model.defaults = function( attrs ){ return this.extend({ defaults : attrs }); };
return Model;
})();
model._pending = false;
model._changing = false;
Nested.Collection = Nested.Model.Collection = ( function(){
var Collection,
CollectionProto = Backbone.Collection.prototype;
return model;
};
function wrapCall( func ){
return function(){
if( !this.__changing++ ){
this.trigger( 'before:change' );
}
// Optimized Backbone Core functions
// =================================
// Deep set model attributes, catching nested attributes changes
function setAttrs( model, attrs, options ){
model.__begin();
var res = func.apply( this, arguments );
applyTransform( model, attrs, model.__attributes, options );
if( !--this.__changing ){
this.trigger( 'after:change' );
}
model.__commit( attrs, options );
return res;
};
return model;
}
// transform attributes hash
function applyTransform( model, attrs, attrSpecs, options ){
for( var name in attrs ){
var attrSpec = attrSpecs[ name ], value = attrs[ name ];
if( attrSpec ){
attrs[ name ] = attrSpec.transform( value, options, model, name );
}
else{
error.unknownAttribute( model, name, value );
}
}
}
Collection = Backbone.Collection.extend({
triggerWhenChanged: 'change add remove reset', // sort
__class : 'Collection',
function __begin(){
this.__duringSet++ || ( this.__nestedChanges = {} );
}
model : Nested.Model,
function __commit( a_attrs, options ){
var attrs = a_attrs;
isValid : function( options ){
return this.every( function( model ){
return model.isValid( options );
});
},
if( !--this.__duringSet ){
var nestedChanges = this.__nestedChanges,
attributes = this.attributes;
deepClone: function(){
var copy = CollectionProto.clone.call( this );
attrs || ( attrs = {} );
copy.reset( this.map( function( model ){
return model.deepClone();
} ) );
// Catch nested changes.
for( var name in nestedChanges ){
var value = name in attrs ? attrs[ name ] : attrs[ name ] = nestedChanges[ name ];
return copy;
},
if( value === attributes[ name ] ){
// patch attributes to force change:name event
attributes[ name ] = null;
}
}
__changing: 0,
this.__nestedChanges = {};
}
set: wrapCall( function( models, options ){
if( models && models instanceof Nested.Collection ){
models = models.models;
if( attrs ){
bbSetAttrs( this, attrs, options );
}
}
},{"./backbone+":2,"./errors":4,"underscore":"underscore"}],8:[function(require,module,exports){
/* Object extensions: backbone-style OO functions and helpers...
* (c) Vlad Balin & Volicon, 2015
* ------------------------------------------------------------- */
(function( spec ){
for( var name in spec ){
Object[ name ] || Object.defineProperty( Object, name, {
enumerable : false,
configurable : true,
writable : true,
value : spec[ name ]
} );
}
})( {
// Object.assign polyfill from MDN.
assign : function( target, firstSource ){
if( target == null ){
throw new TypeError( 'Cannot convert first argument to object' );
}
var to = Object( target );
for( var i = 1; i < arguments.length; i++ ){
var nextSource = arguments[ i ];
if( nextSource == null ){
continue;
}
var keysArray = Object.keys( Object( nextSource ) );
for( var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++ ){
var nextKey = keysArray[ nextIndex ];
var desc = Object.getOwnPropertyDescriptor( nextSource, nextKey );
if( desc !== void 0 && desc.enumerable ){
to[ nextKey ] = nextSource[ nextKey ];
}
return CollectionProto.set.call( this, models, options );
}),
remove: wrapCall( CollectionProto.remove ),
add: wrapCall( CollectionProto.add ),
reset: wrapCall( CollectionProto.reset ),
sort: wrapCall( CollectionProto.sort ),
}
}
return to;
},
getModelIds : function(){
return _.pluck( this.models, 'id' );
// Object.transform function, similar to _.mapObject
transform : function( dest, source, fun, context ){
for( var name in source ){
if( source.hasOwnProperty( name ) ){
var value = fun.call( context, source[ name ], name );
typeof value === 'undefined' || ( dest[ name ] = value );
}
});
}
Collection.extend = createExtendFor( Collection );
Collection.defaults = function( attrs ){
return this.prototype.model.extend({ defaults : attrs }).Collection;
return dest;
},
// get property descriptor looking through all prototype chain
getPropertyDescriptor : function( obj, prop ){
for( var desc; !desc && obj; obj = Object.getPrototypeOf( obj ) ){
desc = Object.getOwnPropertyDescriptor( obj, prop );
}
return desc;
},
// extend function in the fashion of Backbone, with extended features required by NestedTypes
// - supports native properties definitions
// - supports forward declarations
// - warn in case if base class method is overriden with value. It's popular mistake when working with Backbone.
extend : (function(){
var error = {
overrideMethodWithValue : function( Ctor, name, value ){
console.warn( '[Type Warning] Base class method overriden with value in Object.extend({ ' + name +
' : ' + value + ' }); Object =', Ctor.prototype );
}
};
return Collection;
})();
function Class(){
this.initialize.apply( this, arguments );
}
Nested.options.Type.extend({
isBackboneType : true,
isModel : true,
// Backbone-style extend with native properties and late definition support
function extend( protoProps, staticProps ){
var Parent = this === Object ? Class : this,
Child;
_name : '',
handleNestedChange : function(){},
if( typeof protoProps === 'function' ){
Child = protoProps;
protoProps = null;
}
else if( protoProps && protoProps.hasOwnProperty( 'constructor' ) ){
Child = protoProps.constructor;
}
else{
Child = function Constructor(){ return Parent.apply( this, arguments ); };
}
properties : {
name : {
set : function( name ){
this._name = name;
Object.assign( Child, Parent );
// (!) this handler will be called in the context of model
this.handleNestedChange = function(){
var value = this.attributes[ name ];
Child.prototype = Object.create( Parent.prototype );
Child.prototype.constructor = Child;
Child.__super__ = Parent.prototype;
if( this.__duringSet ){
this.__nestedChanges[ name ] = value;
}
else{
this.attributes[ name ] = null;
baseModelSet.call( this, name, value );
}
}
},
protoProps && Child.define( protoProps, staticProps );
get : function(){
return this._name;
return Child;
}
function warnOnError( value, name ){
var prop = Object.getPropertyDescriptor( this.prototype, name );
if( prop ){
var baseIsFunction = typeof prop.value === 'function',
valueIsFunction = typeof value === 'function';
if( baseIsFunction && !valueIsFunction ){
error.overrideMethodWithValue( this, name, prop );
}
}
},
delegateEvents : function( model, oldValue, newValue ){
if( this.triggerWhenChanged && oldValue !== newValue ){
var name = this.name;
return value;
}
oldValue && model.stopListening( oldValue );
function preparePropSpec( spec, name ){
var prop = Object.getPropertyDescriptor( this.prototype, name );
if( newValue ){
model.listenTo( newValue, 'before:change', model.__beginChange );
model.listenTo( newValue, 'after:change', model.__commitChange );
model.listenTo( newValue, this.triggerWhenChanged, this.handleNestedChange );
if( prop && typeof prop.value === 'function' ){
error.overrideMethodWithValue( this, name, prop );
}
_.each( model.listening[ name ], function( handler, events ){
var callback = typeof handler === 'string' ? this[handler] : handler;
this.listenTo( newValue, events, callback );
}, this );
}
return spec instanceof Function ? { get : spec } : spec;
}
model.trigger( 'replace:' + name, model, newValue, oldValue );
function define( protoProps, staticProps ){
Object.transform( this.prototype, protoProps, warnOnError, this );
Object.transform( this, staticProps, warnOnError, this );
protoProps && Object.defineProperties( this.prototype,
Object.transform( {}, protoProps.properties, preparePropSpec, this ) );
return this;
}
extend.attach = function(){
for( var i = 0; i < arguments.length; i++ ){
var Ctor = arguments[ i ];
Ctor.extend = extend;
Ctor.define = define;
Ctor.prototype.initialize || ( Ctor.prototype.initialize = function(){} );
}
},
};
create : function( value, options ){
return new this.type( value, options );
extend.attach( Class );
extend.Class = Class;
extend.error = error;
return extend;
})()
} );
module.exports = Object.extend.Class;
},{}],9:[function(require,module,exports){
// Nested Relations
//=================
var bbVersion = require( 'backbone' ).VERSION,
attribute = require( './attribute' ),
Collection = require( './collection' ),
_ = require( 'underscore' );
function parseReference( collectionRef ){
switch( typeof collectionRef ){
case 'function' :
return collectionRef;
case 'object' :
return function(){ return collectionRef; };
case 'string' :
return new Function( 'return this.' + collectionRef );
}
}
exports.from = function( masterCollection ){
var getMaster = parseReference( masterCollection );
function clone( value ){
return value && typeof value === 'object' ? value.id : value;
}
var ModelRefAttribute = attribute.Type.extend( {
toJSON : clone,
clone : clone,
isChanged : function( a, b ){
// refs are equal when their id is equal.
var aId = a && typeof a == 'object' ? a.id : a,
bId = b && typeof b == 'object' ? b.id : b;
return aId !== bId;
},
cast : function( value, options, model ){
var incompatibleType = value != null && !( value instanceof this.type ),
existingModelOrCollection = model.attributes[ this.name ];
get : function( objOrId, name ){
if( typeof objOrId !== 'object' ){
// Resolve reference.
var master = getMaster.call( this );
if( incompatibleType ){
if( existingModelOrCollection ){ // ...delegate update for existing object 'set' method
if( options && options.parse && this.isModel ){ // handle inconsistent backbone's parse implementation
value = existingModelOrCollection.parse( value );
}
if( master && master.length ){
// Silently update attribute with object form master.
objOrId = master.get( objOrId ) || null;
this.attributes[ name ] = objOrId;
existingModelOrCollection.set( value, options );
value = existingModelOrCollection;
// Subscribe for events manually. delegateEvents won't be invoked.
var attrSpec = this.__attributes[ name ];
objOrId && attrSpec.events && this.listenTo( objOrId, attrSpec.events );
}
else{ // ...or create a new object, if it's not exist
value = this.create( value, options );
else{
objOrId = null;
}
}
return value;
},
return objOrId;
}
} );
initialize : function( spec ){
Nested.options.Type.prototype.initialize.apply( this, arguments );
_.isUndefined( this.triggerWhenChanged ) && ( this.triggerWhenChanged = spec.type.prototype.triggerWhenChanged );
var options = attribute( { value : null } );
options.Attribute = ModelRefAttribute; //todo: consider moving this to the attrSpec
return options;
};
this.isModel = this.type.prototype instanceof Nested.Model;
}
}).bind( Nested.Model, Nested.Collection );
var CollectionProto = Collection.prototype;
// Nested Relations
//=================
var refsCollectionSpec = {
triggerWhenChanged : bbVersion >= '1.2.0' ? 'update reset' : 'add remove reset', // don't bubble changes from models
__class : 'Collection.SubsetOf',
var _store = null;
resolvedWith : null,
refs : null,
function parseReference( collectionRef ){
switch( typeof collectionRef ){
case 'function' : return collectionRef;
case 'object' : return function(){ return collectionRef; };
case 'string' : return new Function( 'return this.' + collectionRef );
}
}
toJSON : function(){
return this.refs || _.pluck( this.models, 'id' );
},
Nested.Model.from = Nested.Model.From = Nested.Model.RefTo = ( function(){
return function( masterCollection ){
var getMaster = parseReference( masterCollection );
clone : function( options ){
var copy = CollectionProto.clone.call( this, _.omit( options, 'deep' ) );
copy.resolvedWith = this.resolvedWith;
copy.refs = this.refs;
return Nested.options({
value : null,
return copy;
},
toJSON : function( value ){
return value && typeof value === 'object' ? value.id : value;
},
parse : function( raw ){
var models = [];
get : function( objOrId, name ){
if( this.resolvedWith ){
models = _.compact( _.map( raw, function( id ){
return this.resolvedWith.get( id );
}, this ) );
}
else{
this.refs = raw;
}
if( typeof objOrId !== 'object' ){
var master = getMaster.call( this );
return models;
},
if( master && master.length ){
objOrId = master.get( objOrId ) || null;
this.set( name, objOrId, { silent: true });
}
else{
objOrId = null;
}
}
toggle : function( modelOrId ){
var model = this.resolvedWith.get( modelOrId );
return objOrId;
},
if( this.get( model ) ){
this.remove( model );
}
else{
this.add( model );
}
},
set : function( modelOrId, name ){
if( typeof modelOrId !== 'object' ){
var current = this.attributes[ name ];
if( current && typeof current === 'object' && current.id === modelOrId ) return;
}
addAll : function(){
this.reset( this.resolvedWith.models );
},
removeAll : function(){
this.reset();
},
justOne : function( arg ){
var model = arg instanceof Backbone.Model ? arg : this.resolvedWith.get( arg );
this.set( [ model ] );
},
set : function( models, upperOptions ){
var options = { merge : false };
return modelOrId;
}
});
};
})();
if( models ){
if( models instanceof Array && models.length && typeof models[ 0 ] !== 'object' ){
options.merge = options.parse = true;
}
}
Nested.Collection.SubsetOf = Nested.Collection.subsetOf = Nested.Collection.RefsTo = ( function(){
var CollectionProto = Nested.Collection.prototype;
CollectionProto.set.call( this, models, _.defaults( options, upperOptions ) );
},
var refsCollectionSpec = {
triggerWhenChanged : "add remove reset",
__class : 'Collection.SubsetOf',
resolve : function( collection ){
if( collection && collection.length ){
this.resolvedWith = collection;
resolvedWith : null,
refs : null,
if( this.refs ){
this.reset( this.refs, { silent : true } );
this.refs = null;
}
}
toJSON : function(){
return this.refs || _.pluck( this.models, 'id' );
},
return this;
}
};
deepClone : CollectionProto.clone,
exports.subsetOf = function( masterCollection ){
var SubsetOf = this.__subsetOf || ( this.__subsetOf = this.extend( refsCollectionSpec ) );
var getMaster = parseReference( masterCollection );
parse : function( raw ){
var models = [];
return attribute( {
type : SubsetOf,
if( this.resolvedWith ){
models = _.compact( _.map( raw, function( id ){
return this.resolvedWith.get( id );
}, this ) );
}
else{
this.refs = raw;
}
get : function( refs ){
!refs || refs.resolvedWith || refs.resolve( getMaster.call( this ) );
return refs;
}
} );
};
return models;
},
},{"./attribute":1,"./collection":3,"backbone":"backbone","underscore":"underscore"}],10:[function(require,module,exports){
var Backbone = require( './backbone+' ),
Model = require( './model' ),
Collection = require( './collection' ),
_ = require( 'underscore' );
toggle : function( modelOrId ){
var model = this.resolvedWith.get( modelOrId );
var _store = null;
if( this.get( model ) ){
this.remove( model );
// Exports native property spec for model store
exports.get = function(){ return _store; };
exports.set = function( spec ){
_.each( spec, function( Type, name ){
Type.options && ( spec[name] = Type.options( {
get : function( value ){
if( !this.resolved[name] ){
value.fetch && value.fetch();
this.resolved[name] = true;
}
else{
this.add( model );
}
},
addAll : function(){
this.reset( this.resolvedWith.models );
return value;
},
removeAll : function(){
this.reset();
},
justOne : function( arg ){
var model = arg instanceof Backbone.Model ? arg : this.resolvedWith.get( arg );
this.set( [ model ] );
},
set : function( models, upperOptions ){
var options = { merge : false };
if( models ){
if( models instanceof Array && models.length && typeof models[ 0 ] !== 'object' ){
options.merge = options.parse = true;
}
}
set : function( value ){
value.length || ( this.resolved[name] = false );
return value;
}
} ) );
} );
CollectionProto.set.call( this, models, _.defaults( options, upperOptions ) );
},
var $ = Backbone.$;
resolve : function( collection ){
if( collection && collection.length ){
this.resolvedWith = collection;
var Cache = Model.extend( {
attributes : spec,
resolved : {},
if( this.refs ){
this.reset( this.refs, { silent : true } );
this.refs = null;
initialize : function(){
this.resolved = {};
this.installHooks();
},
installHooks : function(){
var self = this;
_.each( this.attributes, function( element, name ){
if( !element ){
return;
}
var fetch = element.fetch;
if( fetch ){
element.fetch = function(){
self.resolved[name] = true;
return fetch.apply( this, arguments );
}
}
return this;
}
};
if( element instanceof Collection && element.length ){
this.resolved[name] = true;
}
}, this );
},
return function( masterCollection ){
var SubsetOf = this._subsetOf || ( this._subsetOf = this.extend( refsCollectionSpec ) );
var getMaster = parseReference( masterCollection );
fetch : function(){
var xhr = [],
objsToFetch = arguments.length ? arguments : _.keys( this.resolved );
return Nested.options({
type : SubsetOf,
_.each( objsToFetch, function( name ){
var attr = this.attributes[name];
attr.fetch && xhr.push( attr.fetch() );
}, this );
get : function( refs ){
!refs || refs.resolvedWith || refs.resolve( getMaster.call( this ) );
return refs;
}
});
};
})();
return $ && $.when && $.when.apply( Backbone.$, xhr );
},
Object.defineProperty( Nested, 'store', {
set : function( spec ){
_.each( spec, function( Type, name ){
Type.options && ( spec[ name ] = Type.options({
get : function( value ){
if( !this.resolved[ name ] ){
value.fetch && value.fetch();
this.resolved[ name ] = true;
}
clear : function(){
var attrs = this.defaults();
arguments.length && ( attrs = _.pick( attrs, _.toArray( arguments ) ) );
this.set( attrs );
this.installHooks();
return this;
}
} );
return value;
},
Model.prototype.store = _store = new Cache();
};
set : function( value ){
value.length || ( this.resolved[ name ] = false );
return value;
}
}) );
});
var Cache = Nested.Model.extend({
attributes : spec,
resolved : {},
},{"./backbone+":2,"./collection":3,"./model":6,"underscore":"underscore"}],"nestedtypes":[function(require,module,exports){
// NestedTypes namespace
// =======================
initialize : function(){
this.resolved = {};
this.installHooks();
},
installHooks : function(){
var self = this;
var Model = require( './model' ),
Collection = require( './collection' ),
relations = require( './relations' ),
attribute = require( './attribute' );
_.each( this.attributes, function( element, name ){
var fetch = element.fetch;
if( fetch ){
element.fetch = function(){
self.resolved[ name ] = true;
return fetch.apply( this, arguments );
}
}
require( './metatypes' );
if( element instanceof Nested.Collection && element.length ){
this.resolved[ name ] = true;
}
}, this );
},
Collection.subsetOf = relations.subsetOf;
Model.from = relations.from;
Model.Collection = Collection;
fetch : function(){
var xhr = [],
objsToFetch = arguments.length ? arguments : _.keys( this.resolved );
Object.defineProperty( exports, 'store', require( './store' ) );
_.each( objsToFetch, function( name ){
var attr = this.attributes[ name ];
attr.fetch && xhr.push( attr.fetch() );
}, this );
Object.assign( exports, {
Class : require( './object+' ),
error : require( './errors' ),
attribute : attribute,
options : attribute,
return $.when.apply( $, xhr );
},
value : function( value ){
return attribute({ value: value });
},
clear : function(){
var attrs = this.defaults();
arguments.length && ( attrs = _.pick( attrs, _.toArray( arguments ) ) );
this.set( attrs );
this.installHooks();
return this;
}
});
Collection : Collection,
Model : Model,
Nested.Model.prototype.store = _store = new Cache();
},
get : function(){
return _store;
}
});
}));
defaults : function( x ){
return Model.defaults( x );
}
});
},{"./attribute":1,"./collection":3,"./errors":4,"./metatypes":5,"./model":6,"./object+":8,"./relations":9,"./store":10}]},{},[]);
return require('nestedtypes');
}))
{
"name": "backbone.nested-types",
"main": "nestedtypes.js",
"description": "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.",
"homepage": "https://github.com/Volicon/backbone.nestedTypes",
"keywords": [ "backbone", "relation", "nested", "model", "types", "properties" ],
"repository": {
"type": "git",
"url": "https://github.com/Volicon/backbone.nestedTypes.git"
},
"author": "Vlad Balin <https://github.com/gaperton>",
"contributors": "",
"dependencies": {
"underscore": ">=1.5.0",
"backbone": ">=1.1.2"
},
"files": [
"nestedtypes.js"
],
"license": "MIT",
"version": "0.10.0"
"name": "backbone.nested-types",
"main": "nestedtypes.js",
"description": "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.",
"homepage": "https://github.com/Volicon/backbone.nestedTypes",
"keywords": [
"backbone",
"relation",
"nested",
"model",
"types",
"properties"
],
"repository": {
"type": "git",
"url": "https://github.com/Volicon/backbone.nestedTypes.git"
},
"author": "Vlad Balin <https://github.com/gaperton>",
"contributors": "",
"dependencies": {
"underscore": ">=1.5.0",
"backbone": ">=1.1.2"
},
"devDependencies": {
"uglify-js": "*",
"browserify": "*",
"mocha": "*",
"chai": "* <3",
"sinon": "*",
"sinon-chai": "*"
},
"files": [
"nestedtypes.js",
"nestedtypes.min.js"
],
"license": "MIT",
"version": "1.0.0",
"scripts": {
"test": "node_modules/.bin/mocha",
"minify:win": ".\\node_modules\\.bin\\uglifyjs nestedtypes.js --comments --compress --mangle --screw-ie8 > nestedtypes.min.js & .\\node_modules\\.bin\\uglifyjs .\\chaplinjs\\nestedtypes.js --comments --compress --mangle --screw-ie8 > .\\chaplinjs\\nestedtypes.min.js",
"make:win": "node_modules\\.bin\\browserify -x underscore -x backbone -r ./src/main:nestedtypes > ./umd/_bundle.js && type .\\umd\\copyright.js .\\umd\\head.js .\\umd\\_bundle.js .\\umd\\tail.js > .\\nestedtypes.js && type .\\umd\\copyright.js .\\umd\\head-chaplin.js .\\umd\\_bundle.js .\\umd\\tail.js > .\\chaplinjs\\nestedtypes.js && del .\\umd\\_bundle.js",
"build:win": "npm test && npm run make:win && npm run minify:win",
"minify": "node_modules/.bin/uglifyjs nestedtypes.js --comments --compress --mangle --screw-ie8 > nestedtypes.min.js & node_modules/.bin/uglifyjs ./chaplinjs/nestedtypes.js --comments --compress --mangle --screw-ie8 > ./chaplinjs/nestedtypes.min.js",
"make": "node_modules/.bin/browserify -x underscore -x backbone -r ./src/main:nestedtypes > ./umd/_bundle.js && cat ./umd/copyright.js ./umd/head.js ./umd/_bundle.js ./umd/tail.js > ./nestedtypes.js && ./umd/copyright.js cat ./umd/head-chaplin.js ./umd/_bundle.js ./umd/tail.js > ./chaplinjs/nestedtypes.js && rm ./umd/_bundle.js",
"build": "npm test && npm run make && npm run minify"
}
}

@@ -1,43 +0,39 @@

0.10.0 Release Notes
====================
- attribute get and set hooks
- they can be chained now. Thus, they work on Model.from and Collection.subsetOf
- they takes attribute name as second argument
- fixed bug in set hook (returning undefined didn't prevent attribute modification in some cases)
- Model
- Model.defaults() - added syntax for inline nested model definitions.
- .isValid - fixed exception
- .set now report error when not a plain object is passed as argument.
- .deepClone now pass options through the nested calls
- Model constructor now pass options to the nested constructors in defaults (except 'parse' and 'collection')
- Model.from: fixed bug (assignment of the same id to resolved reference caused unnecessary 'change' event)
- Collection:
- Collection.defaults() - added syntax for inline nested collection definitions.
- 'sort' event now doesn't count as nested attribute update, and won't bubble (it caused multiple problems)
- Collection.subsetOf improvements:
- Collections of different types now can be assigned to each other (model arrays will be passed to .set).
- Added set manipulation methods: addAll, removeAll, justOne.
# Getting Started
backbone.nestedTypes
====================
[![Master Build Status](https://travis-ci.org/Volicon/backbone.nestedTypes.svg?branch=master)](https://travis-ci.org/Volicon/backbone.nestedTypes)
[![Develop Build Status](https://travis-ci.org/Volicon/backbone.nestedTypes.svg?branch=develop)](https://travis-ci.org/Volicon/backbone.nestedTypes)
NestedTypes is the type system for JavaScript, implemented on top of Backbone. It solve common architectural problems of Backbone applications, providing simple yet powerful tools to deal with complex nested data structures. Brief feature list:
Version 1.0.0 is here. Highlights:
- Class and Integer types
- *Native properties* for Model attributes, Collection, and Class.
- Inline Collection definition syntax for Models.
- Model.defaults inheritance and deep copying.
- Type declarations and automatic type casts for Model attributes.
- Easy handling of Date attributes.
- *Nested models* and collections.
- *One-to-many* and *many-to-many* models relations.
- 'change' event bubbling for nested models and collections.
- Attribute-level control for parse/toJSON and event bubbling.
- Run-time type error detection and logging.
- New .has type specs syntax
- Huge performance improvement over vanilla backbonejs. Model updates are 4x faster in most browsers (20x faster in Chrome and nodejs).
How it feels like
-----------------
## What it is
It feels much like statically typed programming language. Yet, it's vanilla JavaScript.
NestedTypes is state-of-the-art backbonejs-compatible model framework.
### Complex attribute types
* Cross-browser handling of Date.
* Nested models and collections.
* One-to-many and many-to-many model relationships.
It's achieved using attribute type annotations, which feels in much like statically typed programming language. Yet, this annotations are vanilla JavaScript, no transpiler step is required.
### Safety
NestedTypes check types on every model update and perform dynamic type casts to ensure that attributes will always hold values of proper type.
As result, NestedTypes models are extremely reliable. It's impossible to break client-server protocol with inaccurate attribute assignment. If something will go really wrong, it will warn you with a messages in the console.
### Performance
NestedTypes uses attribute type information for sophisticated optimizations targeting modern JS JIT engines.
Compared to backbonejs, model updates are about 20 times faster in Chrome/nodejs, and 4 times faster in other browsers.
### Easy to use and learn
NestedTypes was originally designed with an idea to make backbonejs more friendly for newbiews.
What we do, is taking intuitive newbie approach to backbonejs, and turn it from the mistake to legal way of doing things.
```javascript

@@ -47,21 +43,21 @@ var User = Nested.Model.extend({

attributes : {
defaults : {
// Primitive types
login : String, // = ""
email : String.value( null ), // = null
loginCount : Number.options({ toJSON : false }) // = 0, not serialized
active : Boolean.value( true ), // = true
login : "", // String
email : String.value( null ), // null, but String
loginCount : Number.has.toJSON( false ) // 0, not serialized
active : Boolean.value( true ), // true
created : Date, // = new Date()
created : Date, // new Date()
settings : Settings, // nested model
settings : Settings, // new Settings()
// collection of models, received as an array of model ids
roles : Role.Collection.SubsetOf( rolesCollection ),
roles : Role.Collection.subsetOf( rolesCollection ),
// reference to model, received as model id.
office : Office.From( officeCollection )
office : Office.from( officeCollection )
}
});
var collection = new User.Collection(); // Collection is already there...
var collection = new User.Collection();
collection.fetch().done( function(){

@@ -74,5 +70,4 @@ var user = collection.first();

```
> Types are being checked in run-time on assignment, but instead of throwing exceptions it tries to cast values to defined types.
Types are being checked in run-time on assignment, but instead of throwing exceptions it tries to cast values to defined types. For example:
```javascript

@@ -91,44 +86,234 @@ user.login = 1;

```
## Installation & Requirements
> CommonJS (node.js, browserify):
Requirements & Installation
---------------------------
```javascript
var Nested = require( 'nestedtypes' );
```
All modern browsers and IE9+ are supported. To install, type
> CommonJS/AMD (RequireJS).
> 'backbone' and 'underscore' modules must be defined in config paths.
bower install backbone.nested-types
```javascript
require([ 'nestedtypes' ], function( Nested ){ ... });
```
or
> Browser's script tag
npm install backbone.nested-types
```html
<script src="underscore.js" type="text/javascript"></script>
<script src="backbone.js" type="text/javascript"></script>
<script src="nestedtypes.js" type="text/javascript"></script>
<script> var Model = Nested.Model; ... </script>
```
or just copy 'nestedtypes.js' file to desired location.
### Supported JS environments
NestedTypes requires modern JS environment with support for native properties.
It's tested in `IE 9+`, `Chrome`, `Safari`, `Firefox`, which currently gives you about 95%
of all browsers being used for accessing the web.
NestedTypes is compatible with node.js, CommonJS/AMD (e.g. RequireJS) module loaders, and could be included with plain script tag as well. To include it, use
`node.js` and `io.js` are also supported.
var NestedTypes = require( 'nestedtypes');
### Packaging and dependencies
or
NestedTypes itself is packaged as UMD (Universal Module Definition) module, and should load dependencies properly in any environment.
require([ 'nestedtypes' ], function( NestedTypes ){
NestedTypes require `underscore` and `backbone` libraries. They either must be included globally with `<script>`tag or, if `CommonJS`/`AMD` loaders are used, be accessible by their standard module names.
or
### bower
<script src="nestedtypes.js" type="text/javascript"></script>
`bower install backbone.nested-types`
### npm
# API Reference
## Basic features
### Model.defaults:
- Models.attributes as an alternative to 'defaults'
- Native properties are created for every entry.
- Entries are inherited from the base Model.defaults/attributes.
- JSON literals will be deep copied upon creation of model.
- attributes *must* be declared in defaults/attributes.
`npm install backbone.nested-types`
'defaults' spec may be a function or object, 'attributes' *must* be an object.
### Manual
Copy `nestedtypes.js` file to desired location.
# Object.extend
## Overview
NestedTypes core functionality relies on improved `Object.extend` function, which is also available as separate module
without any side dependencies. It compatible with Backbone's `extend`, while providing some
additional capabilities important for NestedTypes and its applications, such as:
* Native properties
* Forward declarations
You can attach it to your Constructor function like this:
`Object.extend.attach( MyConstructor1, MyConstructor2, ... );`
`Object.extend` can also be used directly to create classes.
<aside class="notice">
NestedTypes attaches <b>Object.extend</b> to all Backbone's classes, thus you may use features described in this chapter with any of your View, Model, Collection, and other Backbone types.
</aside>
When used as a part of NestedTypes,
all `Object.extend` classes also implements `Backbone.Events`, thus your custom objects are capable of sending and receiving backbone events.
You can add your own methods to all classes like this:
`Object.extend.Class.prototype.myMethod = function(){...}`
## Defining classes
```javascript
var MyClass = Object.extend({
a : 1,
inc : function(){ return this.a++; },
initialize : function( x ){
this.a = x;
}
},{
factory : function( x ){
return new MyClass( x );
}
});
```
When executed directly,
`Object.extend( protoProps, staticProps )` creates constructor function and extends
its prototype with `protoProps` properties, also attaching `staticProps` to the constructor
itself. Constructor will call optional `initialize` method.
## Inheritance
```javascript
var Subclass = MyClass.extend({
b : 2,
initialize : function( a, b ){
Subclass.__super__.initialize.apply( this, arguments );
this.b = b;
}
}
```
Every constructor created with `Object.extend` may be further extended with `extend` method.
Correct prototype chain will be built and attached to subclass constructor. Every subclass
constructor has `__super__` property pointing to the prototype of the base class.
## Overriding constructor
```javascript
var Subclass = MyClass.extend({
b : 2,
constructor : function( a, b ){
MyClass.apply( this, arguments );
this.b = b;
}
}
```
You may override constructor instead of dealing with `initialize` function.
##Native Properties
```javascript
var Class = Object.extend({
properties: {
readOnly : function(){ return 'hello!'; },
readWrite : {
get : function(){ return this._value2; },
set : function( value ){
this._value2 = value;
}
}
}
});
```
Native properties can be defined with `properties` spec.
For read-only properties, it's enough to supply get function as spec.
Otherwise, properties specs format is the same as accepted by standard `Object.defineProperties`
function.
You can access native properties as if it would be regular object member variable.
`var x = c.readOnly`
`c.readWrite = 1;`
<aside class="notice">
Native properties are automatically created for model's attributes
</aside>
##Forward declarations
```javascript
var A = Object.extend(),
B = Object.extend( function(){ this.b = 'b'; } );
A.define({
bType : B
});
B.define({
aType : A
});
```
Classes can be created with an `Object.extend()`, and defined later using
`MyClass.define( protoProps, staticProps )` function. It can be helpful
to resolve circular dependencies.
`define` cannot be used to override constructor. It can be achieved by passing
constructor function to `extend`, as it is done for `B` in the example.
<aside class="notice">
Forward declarations are crucial for recursive type-accurate model definitions.
</aside>
##Console Warnings
```javascript
var A = Object.extend({
a : function(){}
});
var B = A.extend({
a : 0 // Warning about type error
});
```
If you try to override base class function with non-function value, `Object.extend`
will notify you about that with a warning to the console. Cause usually it's a mistake.
In this case, you'll see in the console following message:
`[Type Warning] Base class method overriden with value in Object.extend({ a : 0 }); Object = >...`
```javascript
function warning( Ctor, name, value ){
throw new TypeError( 'Whoops...' );
}
Object.extend.error.overrideMethodWithValue = warning;
```
You may override default warning handling assigning our own function to `Object.extend.error.overrideMethodWithValue`.
<aside class="warning">
When used as part of NestedTypes, this and other warning handlers
are located in <b>Nested.error</b> variable.
</aside>
# Nested.Model
## Overview
In NestedTypes model definition's `defaults` section is the *specification* of model's attributes.
`attributes` keyword may be used instead of `defaults`.
In `defaults` or `attributes`, you may specify attribute default value, its type, and different options of
attribute behavior. Refer to corresponding sections of the manual for details.
<aside class="warning">
Every model attribute <b>must</b> be mentioned in <b>Model.defaults</b>
</aside>
In NestedTypes, attribute declaration is mandatory. When you try to set an attribute which doesn't have default value, you'll got an error in the console.
<aside class="warning">
<b>Model.defaults must</b> be an object. Functions are forbidden.
</aside>
## model.defaults( [ attrs ], [ options ] )
```javascript
var UserInfo = Nested.Model.extend({
defaults : {
name : 'test',
name : 'test'
}

@@ -138,3 +323,3 @@ });

var DetailedUserInfo = UserInfo.extend({
attributes : { // <- the same as 'defaults', use whatever you like
attributes : { // <- alternative syntax for 'defaults'
login : '',

@@ -146,25 +331,134 @@ roles : [ 'user' ]

var user = new DetailedUserInfo();
```
// user.get( 'name' ) would be undefined in plain Backbone.
console.assert( user.name === 'test' ); // you still can use 'get', but why...
> In Backbone, 'name' attr is not inherited and would be undefined.
> In NestedTypes it's inherited, and you can access it directly.
```javascript
console.assert( user.name === 'test' );
user.name = 'admin';
```
// In Backbone all models will share the same instance of [ 'user' ] array.
// So, following line will create a bug. Not in NestedTypes.
> In Backbone all models will share the same instance of [ 'user' ] array. Bug.
> In NestedTypes, user.roles is deep copied on creation. Good practice.
```javascript
user.roles.push( 'admin' );
```
### Inline collection definition (Model.collection).
NestedTypes automatically creates `defaults` function for every model
from model attribute's spec. Base model attributes will be inherited.
By the way, our models from previous example has collections defined already.
Following statement can be used to return every model to its original state:
`model.set( model.defaults() )`
`defaults` function accepts optional `attrs` argument with attribute values hash
and fills missing attributes with default values.
### default values deep cloning
When new model is being created, NestedTypes will deep clone
all items (including objects and arrays) from `defaults` object.
<aside class="notice">
You don't need to wrap <b>defaults</b> object in function any more.
</aside>
### Correct defaults inheritance
When extending some existing model definition, NestedTypes will property
merge base model's `defaults`.
<aside class="notice">
You don't need to do manual tricks for <b>defaults</b> inheritance.
</aside>
## model.attrName
NestedTypes creates native property for every attribute.
`model.attr = val;` has the same effect as `model.set( 'attr', val );`
`val = model.attr;` has the same effect as `val = model.get( 'attr' );`
<aside class="notice">
Accessing individual attributes with native properties is significantly
faster than using <b>get</b> and <b>set</b>.
</aside>
You still might need to use `model.set` in cases when you want to set multiple attributes
at once, or to pass some options.
## model.id
In NestedTypes, `model.id` is assignable property, linked to `model.attributes[ model.idAttribute ]`.
`model.id = 5` has the same effect as `model.set( model.idAttribute, 5 )`
## model.properties
```javascript
var users = new UserInfo.Collection();
var detailedUsers = new DetailedUserInfo.Collection();
var M = Nested.Model.extend({
defaults : {
a : 1
},
properties : {
b : function(){ return this.a + 1; }
}
});
var m = new M();
console.log( m.b ); // 2
```
Custom native properties specification. Most typical use case is calculated properties.
Every model definition creates Collection type extending base Model.Collection. Collection.model and Collection.url properties are taken from model. You could customize collection with a spec in Model.collection, which then will be passed to BaseModel.Collection.extend.
`model.properties` is the part of `Object.extend` functionality. Refer to `Object.extend` manual section for details.
## model.set()
Set model attributes. In NestedTypes, this operation is *type safe*. It's guaranteed that
model attribute will always hold null or value of specified type.
1. Values are converted to proper types. For existing nested models and collections `deep update` may be
invoked. Refer to `Attribute Types` manual section for details.
2. Set hooks are being executed for changing attributes. Refer to `Attribute Options` section for details.
3. Events are being registered for changing attributes. `replace:attr` events are fired,
4. Attribute values are being set, firing regular change events.
On attempt to set an attribute which is not defined, warning message will be printed to console.
In NestedTypes, you can assign individual model attributes directly, and it's faster than using `set`:
`model.attr = val;`
## model.get( 'attr' )
Get attribute value by name. Returned value can be modified with `get hook` in attribute definition.
In NestedTypes, you can access model attributes directly, and it's faster than `get`:
`val = model.attr;`
## Deep clone
`model.deepClone()` or `model.clone({ deep : true })`
Deeply clone model with all nested models, collections, and other complex types.
## Deep get and set
`x = model.deepGet( 'attr1.attr2.modelId.attr3.objId' )`
Get attribute by dot-separated path.
Model attribute name, model.id or model.cid (for collection attribute), index (for array), or object property name ( for plain objects) may be used as an elements of the path.
If some model in the middle of path doesn't exists, it will return `undefined`.
`x = model.deepSet( 'attr1.attr2.modelId.attr3.objId', x )`
Set model value by dot-separated path. If model attribute in the middle of path equals to null, empty model will be created.
## Model.Collection
```javascript
var DetailedUserInfo = UserInfo.extend({
urlBase : '/api/detailed_user/',
var UserInfo = Nested.Model.extend({
urlBase : '/api/user/',

@@ -183,132 +477,157 @@ defaults : {

/*
DetailedUserInfo.Collection = UserInfo.Collection.extend({
url : '/api/detailed_user/',
model : DetailedUserInfo,
initialize: function(){
this.fetch();
}
});
*/
var collection = new UserInfo.Collection();
```
### Class type, which can be extended and can throw/listen to events.
Every model definition has its own correct `Collection` type extending base `Model.Collection`, which can be
accessed instantly without declaration. `Collection.model` and `Collection.url` properties are taken from model.
`var collection = new AnyModel.Collection();`
You could customize collection definition providing the spec in `Model.collection`, which then will be passed to `BaseModel.Collection.extend`.
## Model.define()
```javascript
var A = Nested.Class.extend({
a : 1,
var Tree = Nested.Mode.extend();
initialize : function( options ){
this.listenTo( options.other, 'event', doSomething )
},
Tree.define({
defaults : {
branches : Tree.Collection
}
});
```
Forward declarations makes possible type-accurate recursive and mutually recursive model definitions.
doSomething : function(){
this.trigger( 'something' );
}
});
`Model.define` is the part of `Object.extend` functionality. Refer to `Object.extend` manual section for details.
var B = A.extend({
b : 2,
## Serialization
### model.toJSON
```javascript
var M = Nested.Model.extend({
defaults : {
// Attribute-level toJSON.
a : String.has.toJSON( false ),
b : 5
},
initialize : function( options ){
A.prototype.initialize.apply( this, arguments );
this.listenTo( options.another, 'event', doSomething )
},
});
// Model-level toJSON.
toJSON : function(){
// Call NestedTypes serialization algorithm.
var json = Nested.Model.prototype.toJSON.apply( this, arguments );
var b = new B( options );
// Do some json transformations...
return json;
}
});
```
### Explicit native properties definition (Model, Class, Collection).
All nested attributes will be serialized automatically.
Native properties are generated for model attributes, however, they also can be defined explicitly for Model, Class, Collection with 'properties' specification.
<aside class="notice">
Normally, you don't need to override this method.
</aside>
For Model, explicit property will override generated one, and "properties : false" disable defaults native properties generation.
You can control serialization of any attribute with `toJSON` attribute option. Most typical use case is to exclude attribute from those which are being sent to the server.
### model.parse
```javascript
var A = Nested.Model.extend({
defaults : {
a : 1,
b : 2
},
var M = Nested.Model.extend({
defaults : {
// Attribute-level parse transform.
a : AbstractModel.has.parse( AbstractModel.factory )
},
properties : {
c : function(){
return this.a + this.b;
},
// Model-level parse transform.
parse : function( resp ){
// Do some resp transformations...
ax2 : {
get : function(){
return this.a * 2;
},
// (!) Call attribute-level parse transform (!)
return this._parse( resp );
}
});
```
All nested attributes will be parsed automatically.
set : function( value ){
this.a = value / 2;
return value;
}
}
}
});
<aside class="notice">
Normally, you don't need to override this method.
</aside>
var a = new A();
console.assert( a.c === 3 );
You can customize parsing of any attribute with `parse` attribute option. Most typical use case is to create proper model subclass for abstract model attribute.
a.ax2 = 4;
console.assert( a.c === 2 );
```
You may need to override model-level `parse` function in order to change attribute names or top-level format.
### Run-time errors
<aside class="warning">
If you override <b>model.parse</b>, you have to call <b>this._parse</b>
(or Nested.Model.prototype.parse) to make attribute's <b>parse option</b> work.
</aside>
NestedTypes detect three error types in the runtime, which will be logged to console using console.error.
# Attribute Types
## Generic Constructor types
```javascript
var A = Nested.Model.extend({
defaults : {
obj1 : Ctor, // = new Ctor()
obj2 : Ctor.value( null ), // = null
obj3 : Ctor.value( something ), // = new Ctor( something )
}
});
```
[Type error](Model.extend) Property "name" conflicts with base class members.
```
It's forbidden for native properties to override members of the base Model. Since native properties are generated for Model.defaults elements, its also forbidden to have attribute names which are the same as members of the base Model.
var a = A();
```
[Type Error](Model.set) Attribute hash is not an object: ...
```
First argument of Model.set must be either string, or literal object representing attribute hash.
a.obj2 = "dsds"; // a.obj2 = new Ctor( "dsds" );
console.assert( a.obj2 instanceof Ctor );
```
[Type Error](Model.set) Attribute "name" has no default value.
```
Attempt to set attribute which is not declared in defaults.
### Type spec format
Type specs may be used instead of init values in `Model.defaults`. They looks like this:
## Model.defaults Type Specs
### Basic type annotation syntax and rules
`name : Constructor` or `name : Constructor.value( x )`
IMPORTANT! Model.defaults must be an object to use attribute type annotations features described here.
defaults function body is supported for backward compatibility with backbone only, in order to simplify transition.
where `Constructor` is JS constructor function, and `x` is `null` or value passed
as constructor's argument.
Type specs can be optionally used instead of init values in Model.defaults. They looks like this:
When value is not given, typed attribute is initialized invoking `new Constructor()`.
name : Constructor
### Type casting rules
When typed attribute is assigned with the value...
or
* ...which is `null`, attribute value will be set to `null`.
* ...which is an instance of `Constructor`, attribute's value will be replaced with a given one.
* in other case, NestedTypes will try to convert value to the `Constructor` type, typically invoking `new Constructor( value )`. Procedure might be more complex for some selected types,
such as nested models and collections.
name : Constructor.value( x )
<aside class="notice">
It's guaranteed that attribute will always hold either `null` or instance of `Constructor` type.
</aside>
where Constructor is JS constructor function, and x is its default value.
### Serialization
When default value is not specified, typed attribute is initialized invoking 'new Constructor()'.
Constructor types are being serialized with `JSON.stringify()` method. You may override `toJSON` *for your type*
to customize serialization. I.e.
As a general rule, when typed attribute is assigned with the value...
- which is null, attribute will be set to null.
- which is an instance of Constructor, attribute's value will be replaced.
- in other case, NestedTypes will try to convert value to the Constructor type, typically invoking "new Constructor( value )". This type conversion algorithm may be overriden for some selected types.
`this.name.toJSON()`
When receiving data from server, type cast logic is used to parse JSON responce; typically you don't need to override Model.parse.
will be invoked to produce JSON, if this method exists.
When sending data to the server, Constructor.toJSON will be invoked to produce JSON for typed attributes, so you don't need to override Model.toJSON for that.
When receiving data from server, standard type cast logic is used to convert JSON response to Constructor object.
I.e.
`this.name = new Constructor( jsonResponse )`
will be invoked.
<aside class="notice">
Normally, you don't need to override model.toJSON() and model.parse().
You're encouraged to properly implement constructor and toJSON of your custom type.
</aside>
## Date type
```javascript
var A = Nested.Model.extend({
defaults : {
obj1 : Ctor, // = new Ctor()
obj2 : Ctor.value( null ), // = null
obj3 : Ctor.value( something ), // = new Ctor( something )
created : Date, // = new Date()
updated : Date.value( null ), // = null
a : Date.value( 327943789 ), // = new Date( 327943789 )
b : Date.value( "2012-12-12 12:12" ) // = new Date( "2012-12-12 12:12" )
}

@@ -319,17 +638,48 @@ });

a.obj2 = "dsds"; // a.obj2 = new Ctor( "dsds" );
a.updated = '2012-12-12 12:12';
console.assert( a.updated instanceof Date );
console.assert( a.obj2 instanceof Ctor );
a.updated = '/Date(32323232323)/';
console.assert( a.updated instanceof Date );
```
### Primitive types (Boolean, Number, String, Integer)
### Type spec format
To create attribute of `Date` type, pass `Date` constructor instead of default value.
Primitive types are special in a sense that *they are inferred from their values*, so they are always typed. In most cases special type annotation syntax is not really required.
`time : Date` or `time : Date.value( x )`
It means that if attribute has default value of 5 *then it's guaranteed to be Number or null* (it will be casted to Number on assignments). This is quite different from original Backbone's behaviour which you might expect, and it makes models safer. For polimorphic attributes holding different types you can disable type inference using 'Nested.value'.
When default value is given, it will be converted to Date using type casting rules.
IMPORTANT! Although it's not possible to use type annotations in Model.defaults function body, primitive types will be inferred from their values in this case. So beware.
### Type casting rules
NestedTypes adds global Integer type, to be used in type annotations. It behaves the same as Number, but convert values to integer on attribute assignment using Math.round. Integer type is not being inferred from the values, and needs to be specified explicitly.
* `Number` is treated as milliseconds from 1970 timestamps, as returned by Date.getTime().
* `String` is treated as one of the following date-time formats (will be detected automatically):
* UTC ISO time string.
* Local date-time string.
* Microsoft `/Date(msecs)/` time string.
* `null` sets attribute to `null` bypassing type conversion logic.
* Other values will be converted to `Invalid Date`.
<aside class="notice">
<b>NestedTypes</b> is capable of parsing ISO date format properly in all browsers, including Safary.
</aside>
### Serialization
`Date` attributes are serialized to UTC ISO date string by default. You may customize date serialization format
providing attribute's `toJSON` option. Following option will serialize `time` to milliseconds.
`time : Date.has.toJSON( function( date ){ return date.getTime(); })`
You can prevent attribute from being serialized, using:
`time : Date.has.toJSON( false )`
`Date` attributes are being parsed from JSON using type casting rules.
<aside class="notice">
You don't need to override <b>Model.parse</b> or <b>Model.toJSON</b> to handle Date attributes.
</aside>
## Primitive types
```javascript

@@ -373,40 +723,58 @@ var A = Nested.Model.extend({

### Date type
- Automatic parsing of common JSON date representations.
- Automatically serialized to ISO string (don't need to override toJSON)
### Type spec format
Primitive types (Boolean, Number, String) are special in a sense that *they are inferred from their values*. In most cases special type annotation syntax is not really required. For example:
* `n : 5` is the same as `n : Number.value( 5 )`
* `b : true` is the same as `b : Boolean.value( true )`
* `s : 'hi'` is the same as `s : String.value( 'hi' )`
* `x : null` is *not* the same. No type will be being inferred from `null` value.
Date attributes free you from overriding Model.parse or Model.toJSON when you want to transfer dates between server and client.
<aside class="warning">
Even without type annotations, it's guaranteed that attributes will <b>retain the type of default primitive values</b>. This is serious difference from backbonejs behavior, which makes models safer.
Strings and numbers will be converted to date with Date constructor. NestedTypes contains additional logic to implement cross-browser ISO date parsing and handling of MS date format.
You can disable type inference using <b>Nested.value( x )</b> or just specifying <b>null</b> default value.
</aside>
On serialization, Date.toJSON will be invoked for date attribute, producing UTC-0 ISO date string representation.
### Integer type
NestedTypes adds global `Integer` type, to be used in type annotations. `Integer` type is not being inferred from default values, and needs to be specified explicitly.
```javascript
var A = Nested.Model.extend({
defaults : {
created : Date, // = new Date()
updated : Date.value( null ), // = null
a : Date.value( 327943789 ), // = new Date( 327943789 )
b : Date.value( "2012-12-12 12:12" ) // = new Date( "2012-12-12 12:12" )
}
});
### Type casting rules
* `null` will set attribute to `null` for all primitive types.
* `Number` attribute:
* Number( x ) will be invoked to parse numbers.
* Attribute will be set to `NaN` if conversion will fail.
* `Integer` attribute:
* Same as `Number`, but values also converted to integer using `Math.round`.
* `String` attribute:
* Primitive types will be converted to their string representation.
* For objects, `x.toString()` method will be invoked.
* Conversion to string never fails.
* `Boolean` attribute:
* Will be always converted to `true` or `false` using standard JS type cast logic.
var a = A();
### Serialization
Primitives are serialized to JSON directly. You can disable serialization of particular attribute with an option:
a.updated = '2012-12-12 12:12';
console.assert( a.updated instanceof Date );
`x : Integer.value( 5 ).toJSON( false )`
a.updated = '/Date(32323232323)/';
console.assert( a.updated instanceof Date );
```
## Untyped attributes
### Type spec format
To define untyped attribute, use either of these options:
### Nested Models and Collections
- automatic parsing and serialization
- 'deep updates' and 'deep clone'
- 'change' event bubbling
* `u : null`, `u : []`, or `u : {}`.
* Any `u : x` where typeof x === 'object'.
* `u : Nested.value( x )` for value of any type, including primitives.
To define nested model or collection, just annotate attributes with Model or Collection type.
<aside class="notice">
Objects literals will be <b>deep copied</b> on model creation, you don't need to do anything special for it.
</aside>
Note, that Backbone's .clone() method will create shallow copy of the root model, while Model.deepClone() and Collection.deepClone() will clone model and collection with all subitems.
### Type casting rules
None
### Serialization
When serialized, `value.toJSON` function will be invoked if it exists for particular value.
JSON responses are assigned to untyped attributes as is.
## Models and Collections
```javascript

@@ -417,4 +785,4 @@ var User = Nested.Model.extend({

created : Date,
group : GroupModel,
permissions : PermissionCollection
group : Group,
permissions : Permission.Collection
}

@@ -427,57 +795,86 @@ });

Model/Collection type cast behavior depends on attribute value before assignment:
- If attribute value is null, Model/Collection constructor will be invoked as for usual types.
- If attribute already holds model or collection, *deep update* will be performed instead.
### Type spec format
To define nested model or collection, annotate attribute with Model or Collection type:
"Deep update" means that model/collection object itself will remain in place, and 'set' method will be used to perform an update.
`a : MyModel` or `b : MyModel.Collection` or `c : SomeCollection`
I.e. this code:
<aside class="notice">
In NestedTypes Every <b>Model</b> type has corresponding <b>Collection</b> type, which can be
referenced as <b>Model.Collection</b>.
</aside>
### Inline nested Models and Collections definitions
> Inline nested definitions
```javascript
var user = new User();
user.group = {
name: "Admin"
};
var M = Nested.Model.extend({
defaults :{
// define model extending base Nested.Model
nestedModel : Nested.defaults({
a : 1,
//define model extending specified model
b : MyModel.defaults({
// define collection of nested models
items : Nested.Collection.defaults({
a : 1,
b : 2
})
user.permissions = [{ id: 5, type: 'full' }];
})
})
}
})
```
is equivalent of:
Simple models and collections can be defined with special shortened syntax.
It's useful in case of deeply nested JS objects, when you previously preferred plain objects and arrays in place of models and collections. Now you could easily convert them to nested types, enjoying nested changes detection and 'deep update' features.
### Type casting
> Deep update example:
```javascript
user.group.set({
name: "Admin"
};
var user = new User();
// Following assignment...
user.group = { name: "Admin" };
// ...is the same as this:
user.group.set({ name: "Admin" });
// Following assignment...
user.permissions = [{ id: 5, type: 'full' }];
// ...is the same as this:
user.permissions.set( [{ id: 5, type: 'full' }] );
```
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):
```javascript
// Following assignment...
user.group = {
nestedModel : {
deeplyNestedModel : {
attr : 'value'
},
deeplyNestedModel : { attr : 'value' },
attr : 5
}
};
// ...is the same as this, but fire single 'change' event
user.group.nestedModel.deeplyNestedModel.attr = 'value';
user.group.nestedModel.attr = 'value';
```
is almost equivalent of:
```javascript
user.group.nestedModel.deeplyNestedModel.set( 'attr', 'value' );
user.group.nestedModel.set( 'attr', 'value' );
```
When `Model` or `Collection` attribute is assigned with the value...
but it will fire just single `change` event.
* ...which is `null`, attribute value will be set to `null`.
* ...which is an instance of specified `Model`/`Collection`, attribute's value will be replaced with a given one.
* otherwise, if value has incompatible type, and current attribute value...
* ...is `null`, new model or collection will be created taking value as constructor argument.
* ...is existing model or collection, update will be delegated to its `set` method performing `deep update`.
Change events will be bubbled from nested models and collections.
- `change` and `change:attribute` events for any changes in nested models and collections. Multiple `change` events from submodels during bulk updates are carefully joined together, which make it suitable to subscribe View.render to the top model's `change`.
- `replace:attribute` event when model or collection is replaced with new object. You might need it to subscribe for events from submodels.
- It's possible to control event bubbling for every attribute. You can completely disable it, or override the list of events which would be counted as change:
### Serialization
Nested models and collections are serialized as nested JSON. When JSON response is received, they are being constructed or updated according to type case rules.
### Change events bubbling
> Event bubbling:
```javascript

@@ -488,179 +885,233 @@ var M = Nested.Model.extend({

dontBubble : ModelOrCollection.options({ triggerWhanChanged : false })
dontBubble : ModelOrCollection.has.triggerWhanChanged( false )
}),
bubbleCustomEvents : ModelOrCollection.options({
triggerWhanChanged : 'event1 event2 whatever'
}),
bubbleCustomEvents : ModelOrCollection.has
.triggerWhanChanged( 'event1 event2 whatever' )
}
});
```
### Inline nested Models and Collections definitions
Simple models and collections can be defined with special shortened syntax.
Change events will be bubbled from nested models and collections.
It's useful in case of deeply nested JS objects, when you previously preferred plain objects and arrays in place of models and collections. Now you could easily convert them to nested types, enjoying nested changes detection and 'deep update' features.
* `change` and `change:attribute` events for any changes in nested models and collections. Multiple `change` events from submodels during bulk updates are carefully joined together, which make it suitable to subscribe View.render to the top model's `change`.
* `replace:attribute` event when model or collection is replaced with new object. You might need it to subscribe for events from submodels.
* It's possible to control event bubbling for every attribute. You can completely disable it, or override the list of events which would be counted as change:
## Model id references
```javascript
var M = Nested.Model.extend({
defaults :{
nestedModel : Nested.defaults({ // define model extending base Nested.Model
a : 1,
b : MyModel.defaults({ //define model extending specified model
items : Collection.defaults({ // define collection of nested models
a : 1,
b : 2
})
})
})
var User = Nested.Model.extend({
defaults : {
name : String,
roles : Role.Collection.subsetOf( roles ) // <- serialized as array of model ids
location : Location.from( locations ) // <- serialized as model id
}
})
});
var user = new User({ id: 0 });
user.fetch();
```
> Server response: "{ id: 0, name : 'john', roles : [ 1, 2, 3 ], location : 6 }"
### Attribute options
- type and value
- override native property
- override parse/toJSON
```javascript
//ref attributes behaves like normal collections and models.
assert( user.roles instanceof Collection );
assert( user.roles.first() instanceof Role );
assert( user.location.name === "Boston" );
```
Attribute options spec allow for a full control on the attribute options, including 'type' and 'value'. Attribute type specification is the special case of options spec, which, in its most general form, looks like this:
Sometimes it is suitable to serialize model references as an id or an array of ids.
Nested.options({ ... })
NestedTypes provides special attribute data types to transparently handle this situation, as if you
would work with normal nested models and collections.
The relation between short and long forms of attribute options spec is summarized in the table below:
### Model.from
Short form | Long form
-------------------------|-------
Type | Nested.options({ type : Type })
Type.options({ ... }) | Nested.options({ type : Type, ... })
Nested.value( x ) | Nested.options({ value : x })
Type.value( x ) | Nested.options({ type : Type, value: x })
`Model.from` represent reference to the model from existing collection, which is serialized as model id.
`ref : Model.from( masterCollection )`
Both long and short forms of attribute options are chainable. I.e. following constructs are possible:
Attribute may be assigned with model id or model itself. On `get, attribute behaves as Model type. Model id will be resolved to model on first attribute read attempt.
Type.value( x ).options({ ... }) // same as Nested.options({ type : Type, value : x, ... })
Nested.value( x ).options({ ... }) // = Nested.options({ value : x, ... })
Nested.options({ ... }).value( x ) // = Nested.options({ value : x, ... })
...
If master collection is empty and thus reference cannot be resolved, it will defer id resolution and `get` will return `null`. If master collection is not empty, id will be resolved to model from this collection, or `null` if corresponding model doesn't exists.
Available options so far are:
Attribute counts as changed only when different model or id is assigned.
#### type
```
type : Ctor
```
Attribute's type (constructor function). When no type is provided, attribute behaves as regular backbone attribute.
### Collection.subsetOf
#### value
```
value : x
```
Attribute's default value. When type is specified, value will be casted to this type on construction.
`Collection.subsetOf` is a collection of models taken from other 'master' collection. On first access, it will resolve model ids to real models using master collection for lookups.
#### toJSON
```
toJSON : function( attrValue, attrName ){ return attrValue.toJSON(); }
or
toJSON : false
```
When attribute will be serialized as a part of model, given function will be used *instead* of attribute's toJSON.
Function will be executed in the context of the model.
If master collection is empty and thus references cannot be resolved, it will defer id resolution and just return empty collection. If master collection is not empty, it will filter out ids of non-existent models.
Specifying 'false' will prevent attribute form serialization.
`Collection.subsetOf` supports some additional methods:
#### parse
* addAll() - add all models from master collection.
* removeAll() - same as reset().
* toggle( modelOrId ) - toggle specific model in set.
* justOne( modelOrId ) - reset subset to contain just specified model.
`change` events won't be bubbled from models in Collection.subsetOf. Other collection's events will.
### Master Collection
Master collection reference may be:
- direct reference to collection object
- `string`, designating reference to the current model's member relative to 'this'.
- `function`, which returns reference to collection and executed in the context of the model.
# Attribute options
## Type.has
```javascript
var M = Nested.Model.extend({
defaults : {
attr : Date.has
.value( null )
.toJSON( false )
}
});
```
parse : function( data ){ return data; }
Attribute options spec gives you to customize different aspects of attribute behavior, such as:
* attribute serialization control
* nested changes detection
* attribute's get and set
`.value` is an example of attribute option. In order to get access to other options you need to use keyword `.has`. Options specs are chainable, you can specify any sequence of options separated by dot.
## .value( value )
```javascript
var M = Nested.Model.extend({
defaults : {
a : Type.has.value( value ),
b : Type.value( value )
}
});
```
When attribute is parsed as a part of the model, given function will be called *before* calling the attribute's parse.
Attribute's default value. On model construction, `value` will be casted to `Type` applying usual type casting rules.
#### get hook
<aside class="notice">
`.value` option may be used without leading `.has`.
</aside>
get : function( value, attrName ){ return value; }
## .toJSON( function( value, name ) | false )
```javascript
var M = Nested.Model.extend({
defaults : {
a : Type.has.toJSON( function( value, name ){
return value.text;
}),
Called on Model.get in the context of the model, allowing you to modify returned value.
b : Type.has.toJSON( false )
}
});
```
When attribute will be serialized as a part of model, given function will be used *instead* of attribute's toJSON.
#### set hook
Function accepts attribute's `name` and its current `value`, and will be executed in the context of the model, holding an attribute.
set : function( value, attrName ){ return value; }
Passing `false` option will prevent attribute's serialization.
Called on Model.set in the context of the model, allowing you to modify value before set ot cancel setting of the attribute, returning 'undefined'.
## .parse( function( value, name ) )
```javascript
var M = Nested.Model.extend({
defaults : {
a : Type.has.parse( function( value ){
return Type.factory( value );
})
}
});
```
set hook is executed on every attribute change, *after* type cast. So, it's guaranteed that value will be of the correct type.
Attribute-specific `parse` logic, will be executed after model's `parse` method.
For nested models and collections it will be called only in case when model/collection
instance will be replaced, which makes it a perfect place to handle custom events subscriptions.
Function accepts attribute's `name` and response `value`, and will be executed in the context of the model, holding an attribute.
#### property
property : function( name ){
return {
get : function(){
return this.attribute[ name ];
},
This option is useful to parse abstract model attributes, or handle non-standard format of specific attributes.
set: function( value ){
this.set( name, value );
return value;
}
}
## .get( function( value, name ) )
```javascript
var M = Nested.Model.extend({
defaults : {
a : Type.has.get( function( value, name ){
return value;
})
}
});
```
or
Called during `model.get( 'a' )` or `model.a` in the context of the model, allowing you to modify value which will be returned without altering attribute itself.
property : false
Get hook function accepts attribute's `name` and its current `value`, and returns modified value.
This option is used to override attribute's native property. 'false' option will disable native property generation for this attribute.
Multiple get hooks are chainable, and will be applied in specified order.
It's low level, so use it with extreme care.
## .set( function( value, name ) )
```javascript
var M = Nested.Model.extend({
defaults : {
a : Type.has.set( function( value, name ){
return value;
})
}
});
```
#### triggerWhenChanged
Called during attribute's update in the context of the model *after* type cast but *before* an actual set, allowing you to modify set value.
triggerWhenChanged : String
or
triggerWhenChanged : false
<aside class="notice">
Set hook is only called when attribute value is changed. For nested models and collections case, it will be called <b>only in case</b> when instance will be replaced, not in case of <b>deep update</b>.
</aside>
trigger 'change' event on the model when given list of events are triggered by the attribute.
Specify 'false' to turn off event bubbling.
Set hook function accepts attribute's `name` and `value` to be set, and returns modified value, or `undefined` to cancel attribute update.
One-to-many and many-to-many model relations
--------------------------
Multiple set hooks are chainable, and will be applied in specified order.
Sometimes when you have one-to-many and many-to-many relationships between Models, it is suitable to transfer such a relationships from server as arrays of model ids. NestedTypes gives you special attribute data types for this situation.
Returned value will be casted to attribute's type applying standard convertion rules. So, it's guaranteed that attribute's value will always hold the correct type.
## .events( eventsMap )
```javascript
var User = Nested.Model.extend({
var M = Nested.Model.extend({
defaults : {
name : String,
roles : RolesCollection.subsetOf( roles ) // <- serialized as array of model ids
location : Location.from( locations ) // <- serialized as model id
a : Type.has.events({
'isReady isNotReady' : function(){
this.trigger( 'imwatchingyou' );
}
}),
}
});
var user = new User({ id: 0 });
user.fetch(); // <- you'll receive from server "{ id: 0, name : 'john', roles : [ 1, 2, 3 ] }"
...
// however, user.roles behaves like normal collection of Roles.
assert( user.roles instanceof Collection );
assert( user.roles.first() instanceof Role );
```
Collection.subsetOf is a collection of models taken from existing collection. On first access of attribute of this type, it will resolve ids to real models from the given master collection.
Automatically manage events subscription for nested attribute, capable of sending events. Event handlers will be called in the context of of the parent model.
If master collection is empty and thus references cannot be resolved, it will defer id resolution and just return empty collection or null. If master collection is not empty, it will filter out ids of non-existent models.
## .triggerWhenChanged( String | false )
```javascript
var M = Nested.Model.extend({
defaults : {
a : ModelA.has.triggerWhenChanged( 'change myEvent' ),
b : ModelB.has.triggerWhenChanged( false ),
}
});
```
<aside class="notice">
Makes sense only for Model and Collection attributes.
</aside>
Master collection reference may be:
- direct reference to collection object
- string, designating reference to the current model's member relative to 'this'.
- function, which returns reference to collection and executed in the context of the current model.
Override default list of events used for nested changes detection of selected attribute.
Pass `false` option to disable nested changes detection for this attribute.
## Nested.attribute([ optionsHash ])
```javascript
var User = Nested.Model.extend({
var M = Nested.Model.extend({
defaults : {
name : String,
roles : Collection.subsetOf( 'collection.roles' ); // this.collection.roles
location : Location.from( function(){ return this.collection.locations; }); // this.collection.locations
a : Nested.attribute({
value : null,
toJSON : false
}),
b : Nested.attribute()
.value( null )
.toJSON( false )
}

@@ -670,6 +1121,12 @@ });

`Nested.attribute` function returns attribute spec as it appears after `.has`, optionally accepting set of options as a hash.
<aside class="notice">
It provides a way to pass options to typeless attributes.
</aside>
# Nested.store
There's a global store for the collections, which might be useful in case of bi-directional relationships. It's available as a member of Model (this.store), and globally as Nested.store.
Store needs to be initialized with a hash of collections and models type specs. It can be initialized several times. On first access to every member of the store, it will fetch data from the server automatically. You need to take care of update events.
## Initialization
```javascript

@@ -679,3 +1136,3 @@ Nested.store = {

locations : Locations.Collection
}
};

@@ -686,3 +1143,3 @@ var User = Nested.Model.extend({

roles : Collection.subsetOf( 'store.roles' ); // this.store.roles
location : Location.from( function(){ return this.store.locations; }); // this.store.locations
location : Location.from( 'store.locations' }); // this.store.locations
}

@@ -692,10 +1149,61 @@ });

Store behaves as regular model, but provide some additional methods:
- fetch() will update all store members, which are loaded.
- fetch( 'name1', 'name2', ... ) will fetch listed members and return promise.
- clear() will clear all collections in store and return store to allow chained calls.
- clear( 'name1', 'name2', ... ) will clear listed members.
Store needs to be initialized with a hash of collections and models type specs. It can be initialized several times.
Note, that 'change' events won't be bubbled from models in Collection.subsetOf. Other collection's events will.
Format of the spec object is the same as in `Model.defaults`.
For Model.from attribute no model changes will be bubbled.
## Lazy loading
On first access to every member of the store, it will fetch data from the server automatically. You need to take care of update events.
## Nested.store.fetch( 'attr1', ...)
Update all store members, which are currently loaded:
`Nested.store.fetch()`
Fetches store elements with given names:
`Nested.store.fetch( 'name1', 'name2', ... )`
Returns aggregate promise for xhr objects.
## Nested.clear( 'attr1', ... )
Clear all store collection elements:
`Nested.store.clear()`
Clear selected store collections:
`Nested.store.clear( 'name1', 'name2', ... )`
Returns store to allow chained calls.
# Nested.errors
NestedTypes detect four error types in the runtime, which will be logged to console using console.error.
## Method overriden with value
When you override function with non-function value in the subclass, it usually means an error.
This message also warn you on the situation when you made model attribute or property name the same as
some base class method.
`[Type Warning] Base class method overriden with value in Object.extend({ url : [object Object] }); Object = ...`
## Wrong model.set argument
First argument of Model.set must be either string, or literal object representing attribute hash.
Other situation means serious error. Something goes really wrong.
`[Type Error] Attribute hash is not an object in Model.set( "http://0.0.0.0/" ); this = ...`
## Wrong collection.set argument
First argument of Collection.set must be either an Array, literal object, or compatible Model.
Other situation means serious error. Something goes really wrong.
`[Type Error] Wrong argument type in Collection.set( "dsds" ); this = ...`
## Attribute has ho default value
Attempt to set an attribute which is not declared in model `defaults`.
`[Type Error] Attribute has no default value in Model.set( "a", 0 ); this =...`
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