UNPKG

transactional

Version:

Reactive objects with transactional updates and automatic serialization

321 lines (253 loc) 8.95 kB
import { Record } from '../record' export class Collection { static define( spec ){ } _onAdd( model, options ){} _onRemove( model, options ){} // handler for the model change event // needed to update index when id is changed, even in 'ref' collections _onChange( record, options ){ const { idAttribute } = record; if( idAttribute ){ const prev = record._previousAttributes[ idAttribute ], next = record.attributes[ idAttribute ]; if( prev !== next ){ this._byId[ prev ] = void 0; this._byId[ next ] = record; } } // todo: refs must trigger change here. onChildrenChange win't be called. } // only owner is changed, not refs _onChildrenChange( record, options ){ const isRoot = begin( this ); if( !options.silent ){ this._pending = options; // todo: trigger 'change' event. } isRoot && commit( this, options ); } } var Commons = require( './collections/commons' ), toModel = Commons.toModel, dispose = Commons.dispose, ModelEventsDispatcher = Commons.ModelEventsDispatcher; var Add = require( './collections/add' ), MergeOptions = Add.MergeOptions, add = Add.add, set = Add.set, emptySet = Add.emptySet; var Remove = require( './collections/remove' ), removeOne = Remove.removeOne, removeMany = Remove.removeMany; // transactional wrapper for collections function transaction( func ){ return function(){ this.__changing++ || ( this._changed = false ); var res = func.apply( this, arguments ); if( !--this.__changing && this._changed ){ this._changeToken = {}; trigger1( this, 'changes', this ); } return res; }; } // wrapper for standard collections modification methods // wrap call in transaction and convert singular args function method( method ){ return function( a_models, a_options ){ this.__changing++ || ( this._changed = false ); var options = a_options || {}, models = options.parse ? this.parse( a_models, options ) : a_models; var res = models ? ( models instanceof Array ? method.call( this, models, options ) : method.call( this, [ models ], options )[ 0 ] ) : method.call( this, [], options ); if( !--this.__changing && this._changed ){ this._changeToken = {}; options.silent || trigger1( this, 'changes', this ); } return res; } } function handleChange(){ if( this.__changing ){ this._changed = true; } else{ this._changeToken = {}; trigger1( this, 'changes', this ); } } function SilentOptions( a_options ){ var options = a_options || {}; this.parse = options.parse; this.sort = options.sort; } SilentOptions.prototype.silent = true; function CreateOptions( options, collection ){ MergeOptions.call( this, options, collection ); if( options ){ _.defaults( this, options ); } } module.exports = Backbone.Collection.extend( { model : Model, _owner : null, _store : null, __changing : 0, _changed : false, _changeToken : {}, _dispatcher : null, properties : { length : { enumerable : false, get : function(){ return this.models.length; } } }, _validateNested : function( errors ){ var models = this.models, length = 0; for( var i = 0; i < models.length; i++ ){ var model = models[ i ], error = model.validationError; if( error ){ errors[ model.cid ] = error; length++; } } return length; }, modelId : function( attrs ){ return attrs[ this.model.prototype.idAttribute || 'id' ]; }, constructor : function( models, a_options ){ var options = a_options || {}; this.__changing = 0; this._changed = false; this._changeToken = {}; this._owner = this._store = null; this.model = options.model || this.model; if( options.comparator !== void 0 ){ this.comparator = options.comparator; } this.models = []; this._byId = {}; if( models ){ this.reset( models, new SilentOptions( options ) ); } this.listenTo( this, this._listenToChanges, handleChange ); this.initialize.apply( this, arguments ); }, getStore : function(){ return this._store || ( this._store = this._owner ? this._owner.getStore() : this._defaultStore ); }, sync : function(){ return this.getStore().sync.apply( this, arguments ); }, isValid : function( options ){ return this.every( function( model ){ return model.isValid( options ); } ); }, // Toggle model in collection toggle : function( model, a_next ){ var prev = Boolean( this.get( model ) ), next = a_next === void 0 ? !prev : Boolean( a_next ); if( prev !== next ){ if( prev ){ this.remove( model ); } else{ this.add( model ); } } return next; }, get : function( obj ){ if( obj == null ){ return void 0; } if( typeof obj === 'object' ){ return this._byId[ obj[ this.model.prototype.idAttribute ] ] || this._byId[ obj.cid ]; } return this._byId[ obj ]; }, set : method( function( models, options ){ return this.length ? set( this, models, options ) : emptySet( this, models, options ); } ), reset : method( function( a_models, a_options ){ var options = a_options || {}, previousModels = dispose( this ); var models = emptySet( this, a_models, new SilentOptions( options ) ); options.silent || trigger2( this, 'reset', this, _.defaults( { previousModels : previousModels }, options ) ); return models; } ), // Add a model to the end of the collection. push : function( model, options ){ return this.add( model, _.extend( { at : this.length }, options ) ); }, add : method( function( models, options ){ return this.length ? add( this, models, options ) : emptySet( this, models, options ); } ), sort : transaction( CollectionProto.sort ), // Methods with singular fast-path //------------------------------------------------ // Remove a model, or a list of models from the set. remove( models, options = {} ){ const isRoot = begin( this ), result = models instanceof Array ? removeMany( this, models, options ) : removeOne( this, models, options ); isRoot && commit( this ); return result; }, _onModelEvent : function( event, model, collection, options ){ // lazy initialize dispatcher... var dispatcher = this._dispatcher || ( this._dispatcher = new ModelEventsDispatcher( this.model ) ), handler = dispatcher[ event ] || trigger3; handler( this, event, model, collection, options ); }, at( index ){ const { models } = this; return models[ index < 0 ? index + models.length : index ]; }, clone( options ){ const deep = !options || options.deep, models = deep ? this.map( record => record.clone( options ) ) : this.models; return new this.constructor( models, { model : this.model, //todo ??? comparator : this.comparator } ); }, transaction : function( func, self, args ){ return transaction( func ).apply( self || this, args ); }, // support for relations getModelIds(){ return this.map( record => record.id ); }, createSubset( models, options ){ var SubsetOf = this.constructor.subsetOf( this ).createAttribute().type; var subset = new SubsetOf( models, options ); subset.resolve( this ); return subset; } }, { // 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; } } );