Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #140 from ckeditor/t/132
Browse files Browse the repository at this point in the history
Feature: Two–way data binding between Collection instances. Closes #132.
  • Loading branch information
Piotr Jasiun authored Mar 22, 2017
2 parents b878949 + baed499 commit 6b79624
Show file tree
Hide file tree
Showing 2 changed files with 376 additions and 54 deletions.
156 changes: 105 additions & 51 deletions src/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,36 @@ export default class Collection {
this._idProperty = options && options.idProperty || 'id';

/**
* A helper mapping external items from bound collection ({@link #bindTo})
* and actual items of the collection.
* A helper mapping external items of a bound collection ({@link #bindTo})
* and actual items of this collection. It provides information
* necessary to properly remove items bound to another collection.
*
* See {@link #_bindToInternalToExternalMap}.
*
* @protected
* @member {Map}
* @member {WeakMap}
*/
this._bindToExternalToInternalMap = new WeakMap();

/**
* A helper mapping items of this collection to external items of a bound collection
* ({@link #bindTo}). It provides information necessary to manage the bindings, e.g.
* to avoid loops in two–way bindings.
*
* See {@link #_bindToExternalToInternalMap}.
*
* @protected
* @member {WeakMap}
*/
this._bindToInternalToExternalMap = new WeakMap();

/**
* A collection instance this collection is bound to as a result
* of calling {@link #bindTo} method.
*
* @protected
* @member {module:utils/collection~Collection} #_bindToCollection
*/
this._boundItemsMap = new Map();
}

/**
Expand Down Expand Up @@ -226,6 +249,10 @@ export default class Collection {
this._items.splice( index, 1 );
this._itemMap.delete( id );

const externalItem = this._bindToInternalToExternalMap.get( item );
this._bindToInternalToExternalMap.delete( item );
this._bindToExternalToInternalMap.delete( externalItem );

this.fire( 'remove', item );

return item;
Expand Down Expand Up @@ -271,9 +298,15 @@ export default class Collection {
}

/**
* Removes all items from the collection.
* Removes all items from the collection and destroys the binding created using
* {@link #bindTo}.
*/
clear() {
if ( this._bindToCollection ) {
this.stopListening( this._bindToCollection );
this._bindToCollection = null;
}

while ( this.length ) {
this.remove( 0 );
}
Expand Down Expand Up @@ -351,32 +384,24 @@ export default class Collection {
* console.log( target.get( 0 ).value ); // 'foo'
* console.log( target.get( 1 ).value ); // 'bar'
*
* **Note**: {@link #clear} can be used to break the binding.
*
* @param {module:utils/collection~Collection} collection A collection to be bound.
* @returns {module:ui/viewcollection~ViewCollection#bindTo#using}
* @returns {Object}
* @returns {module:utils/collection~Collection#bindTo#as} return.as
* @returns {module:utils/collection~Collection#bindTo#using} return.using
*/
bindTo( collection ) {
// Sets the actual binding using provided factory.
//
// @private
// @param {Function} factory A collection item factory returning collection items.
const bind = ( factory ) => {
// Load the initial content of the collection.
for ( let item of collection ) {
this.add( factory( item ) );
}

// Synchronize the with collection as new items are added.
this.listenTo( collection, 'add', ( evt, item, index ) => {
this.add( factory( item ), index );
} );

// Synchronize the with collection as new items are removed.
this.listenTo( collection, 'remove', ( evt, item ) => {
this.remove( this._boundItemsMap.get( item ) );
bindTo( externalCollection ) {
if ( this._bindToCollection ) {
/**
* The collection cannot be bound more than once.
*
* @error collection-bind-to-rebind
*/
throw new CKEditorError( 'collection-bind-to-rebind: The collection cannot be bound more than once.' );
}

this._boundItemsMap.delete( item );
} );
};
this._bindToCollection = externalCollection;

return {
/**
Expand All @@ -386,13 +411,7 @@ export default class Collection {
* @param {Function} Class Specifies which class factory is to be initialized.
*/
as: ( Class ) => {
bind( ( item ) => {
const instance = new Class( item );

this._boundItemsMap.set( item, instance );

return instance;
} );
this._setUpBindToBinding( item => new Class( item ) );
},

/**
Expand All @@ -404,29 +423,64 @@ export default class Collection {
* the bound collection items.
*/
using: ( callbackOrProperty ) => {
let factory;

if ( typeof callbackOrProperty == 'function' ) {
factory = ( item ) => {
const instance = callbackOrProperty( item );
this._setUpBindToBinding( item => callbackOrProperty( item ) );
} else {
this._setUpBindToBinding( item => item[ callbackOrProperty ] );
}
}
};
}

/**
* Finalizes and activates a binding initiated by {#bindTo}.
*
* @protected
* @param {Function} factory A function which produces collection items.
*/
_setUpBindToBinding( factory ) {
const externalCollection = this._bindToCollection;

this._boundItemsMap.set( item, instance );
// Adds the item to the collection once a change has been done to the external collection.
//
// @private
const addItem = ( evt, externalItem, index ) => {
const isExternalBoundToThis = externalCollection._bindToCollection == this;
const externalItemBound = externalCollection._bindToInternalToExternalMap.get( externalItem );

// If an external collection is bound to this collection, which makes it a 2–way binding,
// and the particular external collection item is already bound, don't add it here.
// The external item has been created **out of this collection's item** and (re)adding it will
// cause a loop.
if ( isExternalBoundToThis && externalItemBound ) {
this._bindToExternalToInternalMap.set( externalItem, externalItemBound );
this._bindToInternalToExternalMap.set( externalItemBound, externalItem );
} else {
const item = factory( externalItem );

this._bindToExternalToInternalMap.set( externalItem, item );
this._bindToInternalToExternalMap.set( item, externalItem );

this.add( item, index );
}
};

return instance;
};
} else {
factory = ( item ) => {
const instance = item[ callbackOrProperty ];
// Load the initial content of the collection.
for ( let externalItem of externalCollection ) {
addItem( null, externalItem );
}

this._boundItemsMap.set( item, instance );
// Synchronize the with collection as new items are added.
this.listenTo( externalCollection, 'add', addItem );

return instance;
};
}
// Synchronize the with collection as new items are removed.
this.listenTo( externalCollection, 'remove', ( evt, externalItem ) => {
const item = this._bindToExternalToInternalMap.get( externalItem );

bind( factory );
if ( item ) {
this.remove( item );
}
};
} );
}

/**
Expand Down
Loading

0 comments on commit 6b79624

Please sign in to comment.