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

Commit 6b79624

Browse files
author
Piotr Jasiun
authored
Merge pull request #140 from ckeditor/t/132
Feature: Two–way data binding between Collection instances. Closes #132.
2 parents b878949 + baed499 commit 6b79624

File tree

2 files changed

+376
-54
lines changed

2 files changed

+376
-54
lines changed

src/collection.js

Lines changed: 105 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,36 @@ export default class Collection {
5757
this._idProperty = options && options.idProperty || 'id';
5858

5959
/**
60-
* A helper mapping external items from bound collection ({@link #bindTo})
61-
* and actual items of the collection.
60+
* A helper mapping external items of a bound collection ({@link #bindTo})
61+
* and actual items of this collection. It provides information
62+
* necessary to properly remove items bound to another collection.
63+
*
64+
* See {@link #_bindToInternalToExternalMap}.
6265
*
6366
* @protected
64-
* @member {Map}
67+
* @member {WeakMap}
68+
*/
69+
this._bindToExternalToInternalMap = new WeakMap();
70+
71+
/**
72+
* A helper mapping items of this collection to external items of a bound collection
73+
* ({@link #bindTo}). It provides information necessary to manage the bindings, e.g.
74+
* to avoid loops in two–way bindings.
75+
*
76+
* See {@link #_bindToExternalToInternalMap}.
77+
*
78+
* @protected
79+
* @member {WeakMap}
80+
*/
81+
this._bindToInternalToExternalMap = new WeakMap();
82+
83+
/**
84+
* A collection instance this collection is bound to as a result
85+
* of calling {@link #bindTo} method.
86+
*
87+
* @protected
88+
* @member {module:utils/collection~Collection} #_bindToCollection
6589
*/
66-
this._boundItemsMap = new Map();
6790
}
6891

6992
/**
@@ -226,6 +249,10 @@ export default class Collection {
226249
this._items.splice( index, 1 );
227250
this._itemMap.delete( id );
228251

252+
const externalItem = this._bindToInternalToExternalMap.get( item );
253+
this._bindToInternalToExternalMap.delete( item );
254+
this._bindToExternalToInternalMap.delete( externalItem );
255+
229256
this.fire( 'remove', item );
230257

231258
return item;
@@ -271,9 +298,15 @@ export default class Collection {
271298
}
272299

273300
/**
274-
* Removes all items from the collection.
301+
* Removes all items from the collection and destroys the binding created using
302+
* {@link #bindTo}.
275303
*/
276304
clear() {
305+
if ( this._bindToCollection ) {
306+
this.stopListening( this._bindToCollection );
307+
this._bindToCollection = null;
308+
}
309+
277310
while ( this.length ) {
278311
this.remove( 0 );
279312
}
@@ -351,32 +384,24 @@ export default class Collection {
351384
* console.log( target.get( 0 ).value ); // 'foo'
352385
* console.log( target.get( 1 ).value ); // 'bar'
353386
*
387+
* **Note**: {@link #clear} can be used to break the binding.
388+
*
354389
* @param {module:utils/collection~Collection} collection A collection to be bound.
355-
* @returns {module:ui/viewcollection~ViewCollection#bindTo#using}
390+
* @returns {Object}
391+
* @returns {module:utils/collection~Collection#bindTo#as} return.as
392+
* @returns {module:utils/collection~Collection#bindTo#using} return.using
356393
*/
357-
bindTo( collection ) {
358-
// Sets the actual binding using provided factory.
359-
//
360-
// @private
361-
// @param {Function} factory A collection item factory returning collection items.
362-
const bind = ( factory ) => {
363-
// Load the initial content of the collection.
364-
for ( let item of collection ) {
365-
this.add( factory( item ) );
366-
}
367-
368-
// Synchronize the with collection as new items are added.
369-
this.listenTo( collection, 'add', ( evt, item, index ) => {
370-
this.add( factory( item ), index );
371-
} );
372-
373-
// Synchronize the with collection as new items are removed.
374-
this.listenTo( collection, 'remove', ( evt, item ) => {
375-
this.remove( this._boundItemsMap.get( item ) );
394+
bindTo( externalCollection ) {
395+
if ( this._bindToCollection ) {
396+
/**
397+
* The collection cannot be bound more than once.
398+
*
399+
* @error collection-bind-to-rebind
400+
*/
401+
throw new CKEditorError( 'collection-bind-to-rebind: The collection cannot be bound more than once.' );
402+
}
376403

377-
this._boundItemsMap.delete( item );
378-
} );
379-
};
404+
this._bindToCollection = externalCollection;
380405

381406
return {
382407
/**
@@ -386,13 +411,7 @@ export default class Collection {
386411
* @param {Function} Class Specifies which class factory is to be initialized.
387412
*/
388413
as: ( Class ) => {
389-
bind( ( item ) => {
390-
const instance = new Class( item );
391-
392-
this._boundItemsMap.set( item, instance );
393-
394-
return instance;
395-
} );
414+
this._setUpBindToBinding( item => new Class( item ) );
396415
},
397416

398417
/**
@@ -404,29 +423,64 @@ export default class Collection {
404423
* the bound collection items.
405424
*/
406425
using: ( callbackOrProperty ) => {
407-
let factory;
408-
409426
if ( typeof callbackOrProperty == 'function' ) {
410-
factory = ( item ) => {
411-
const instance = callbackOrProperty( item );
427+
this._setUpBindToBinding( item => callbackOrProperty( item ) );
428+
} else {
429+
this._setUpBindToBinding( item => item[ callbackOrProperty ] );
430+
}
431+
}
432+
};
433+
}
434+
435+
/**
436+
* Finalizes and activates a binding initiated by {#bindTo}.
437+
*
438+
* @protected
439+
* @param {Function} factory A function which produces collection items.
440+
*/
441+
_setUpBindToBinding( factory ) {
442+
const externalCollection = this._bindToCollection;
412443

413-
this._boundItemsMap.set( item, instance );
444+
// Adds the item to the collection once a change has been done to the external collection.
445+
//
446+
// @private
447+
const addItem = ( evt, externalItem, index ) => {
448+
const isExternalBoundToThis = externalCollection._bindToCollection == this;
449+
const externalItemBound = externalCollection._bindToInternalToExternalMap.get( externalItem );
450+
451+
// If an external collection is bound to this collection, which makes it a 2–way binding,
452+
// and the particular external collection item is already bound, don't add it here.
453+
// The external item has been created **out of this collection's item** and (re)adding it will
454+
// cause a loop.
455+
if ( isExternalBoundToThis && externalItemBound ) {
456+
this._bindToExternalToInternalMap.set( externalItem, externalItemBound );
457+
this._bindToInternalToExternalMap.set( externalItemBound, externalItem );
458+
} else {
459+
const item = factory( externalItem );
460+
461+
this._bindToExternalToInternalMap.set( externalItem, item );
462+
this._bindToInternalToExternalMap.set( item, externalItem );
463+
464+
this.add( item, index );
465+
}
466+
};
414467

415-
return instance;
416-
};
417-
} else {
418-
factory = ( item ) => {
419-
const instance = item[ callbackOrProperty ];
468+
// Load the initial content of the collection.
469+
for ( let externalItem of externalCollection ) {
470+
addItem( null, externalItem );
471+
}
420472

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

423-
return instance;
424-
};
425-
}
476+
// Synchronize the with collection as new items are removed.
477+
this.listenTo( externalCollection, 'remove', ( evt, externalItem ) => {
478+
const item = this._bindToExternalToInternalMap.get( externalItem );
426479

427-
bind( factory );
480+
if ( item ) {
481+
this.remove( item );
428482
}
429-
};
483+
} );
430484
}
431485

432486
/**

0 commit comments

Comments
 (0)