Procházet zdrojové kódy

Implemented basic 2-way binding.

Aleksander Nowodzinski před 8 roky
rodič
revize
9f46a611e1

+ 52 - 47
packages/ckeditor5-utils/src/collection.js

@@ -63,7 +63,8 @@ export default class Collection {
 		 * @protected
 		 * @member {Map}
 		 */
-		this._boundItemsMap = new Map();
+		this._bindToExternalToInternalMap = new Map();
+		this._bindToInternalToExternalMap = new Map();
 	}
 
 	/**
@@ -352,31 +353,21 @@ export default class Collection {
 	 *		console.log( target.get( 1 ).value ); // 'bar'
 	 *
 	 * @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 {
 			/**
@@ -386,13 +377,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 ) );
 			},
 
 			/**
@@ -404,29 +389,49 @@ export default class Collection {
 			 * the bound collection items.
 			 */
 			using: ( callbackOrProperty ) => {
-				let factory;
-
 				if ( typeof callbackOrProperty == 'function' ) {
-					factory = ( item ) => {
-						const instance = callbackOrProperty( item );
-
-						this._boundItemsMap.set( item, instance );
-
-						return instance;
-					};
+					this._setUpBindToBinding( item => callbackOrProperty( item ) );
 				} else {
-					factory = ( item ) => {
-						const instance = item[ callbackOrProperty ];
+					this._setUpBindToBinding( item => item[ callbackOrProperty ] );
+				}
+			}
+		};
+	}
 
-						this._boundItemsMap.set( item, instance );
+	_setUpBindToBinding( factory ) {
+		const externalCollection = this._bindToCollection;
 
-						return instance;
-					};
-				}
+		// Adds the item to the collection once a change has been done to the external collection.
+		//
+		// @private
+		const addItem = ( evt, externalItem, index ) => {
+			const item = factory( externalItem );
 
-				bind( factory );
+			if ( externalCollection._bindToInternalToExternalMap.has( externalItem ) ) {
+				return;
 			}
+
+			this._bindToExternalToInternalMap.set( externalItem, item );
+			this._bindToInternalToExternalMap.set( item, externalItem );
+			this.add( item, index );
 		};
+
+		// Load the initial content of the collection.
+		for ( let externalItem of externalCollection ) {
+			addItem( null, externalItem );
+		}
+
+		// Synchronize the with collection as new items are added.
+		this.listenTo( externalCollection, 'add', addItem );
+
+		// Synchronize the with collection as new items are removed.
+		this.listenTo( externalCollection, 'remove', ( evt, externalItem ) => {
+			const item = this._bindToExternalToInternalMap.get( externalItem );
+
+			this._bindToExternalToInternalMap.delete( externalItem );
+			this._bindToInternalToExternalMap.delete( item );
+			this.remove( item );
+		} );
 	}
 
 	/**

+ 68 - 0
packages/ckeditor5-utils/tests/collection.js

@@ -725,6 +725,74 @@ describe( 'Collection', () => {
 				} );
 			} );
 		} );
+
+		describe( 'two–way data binding', () => {
+			it( 'works #1', () => {
+				const collectionA = new Collection();
+				const collectionB = new Collection();
+
+				collectionA.name = 'A';
+				collectionB.name = 'B';
+
+				const spyA = sinon.spy();
+				const spyB = sinon.spy();
+
+				collectionA.on( 'add', spyA );
+				collectionA.on( 'add', spyB );
+
+				collectionA.bindTo( collectionB ).using( 'data' );
+				collectionB.bindTo( collectionA ).as( FactoryClass );
+
+				collectionA.add( { value: 'foo' } );
+				collectionA.add( { value: 'bar' } );
+
+				expect( collectionA.map( i => i.value ) ).to.deep.equal( [ 'foo', 'bar' ], 'CollectionA' );
+				expect( collectionB.map( i => i ).every( i => i instanceof FactoryClass ) ).to.be.true;
+				expect( collectionB.map( i => i.data.value ) ).to.deep.equal( [ 'foo', 'bar' ], 'CollectionB' );
+
+				collectionB.add( new FactoryClass( { value: 'baz' } ) );
+
+				expect( collectionA.map( i => i.value ) ).to.deep.equal( [ 'foo', 'bar', 'baz' ], 'CollectionA' );
+				expect( collectionB.map( i => i ).every( i => i instanceof FactoryClass ) ).to.be.true;
+				expect( collectionB.map( i => i.data.value ) ).to.deep.equal( [ 'foo', 'bar', 'baz' ], 'CollectionB' );
+
+				sinon.assert.callCount( spyA, 3 );
+				sinon.assert.callCount( spyB, 3 );
+			} );
+
+			it( 'works #2', () => {
+				const collectionA = new Collection();
+				const collectionB = new Collection();
+
+				collectionA.name = 'A';
+				collectionB.name = 'B';
+
+				const spyA = sinon.spy();
+				const spyB = sinon.spy();
+
+				collectionA.on( 'add', spyA );
+				collectionA.on( 'add', spyB );
+
+				collectionA.bindTo( collectionB ).using( i => i );
+				collectionB.bindTo( collectionA ).using( i => i );
+
+				collectionA.add( new FactoryClass( 'foo' ) );
+				collectionA.add( new FactoryClass( 'bar' ) );
+
+				expect( collectionA.map( i => i.data ) ).to.deep.equal( [ 'foo', 'bar' ], 'CollectionA' );
+				expect( collectionB.map( i => i ).every( i => i instanceof FactoryClass ) ).to.be.true;
+				expect( collectionB.map( i => i.data ) ).to.deep.equal( [ 'foo', 'bar' ], 'CollectionB' );
+
+				collectionB.add( new FactoryClass( 'baz' ) );
+
+				expect( collectionA.map( i => i.data ) ).to.deep.equal( [ 'foo', 'bar', 'baz' ], 'CollectionA' );
+				expect( collectionB.map( i => i ).every( i => i instanceof FactoryClass ) ).to.be.true;
+				expect( collectionB.map( i => i.data ) ).to.deep.equal( [ 'foo', 'bar', 'baz' ], 'CollectionB' );
+
+				sinon.assert.callCount( spyA, 3 );
+				sinon.assert.callCount( spyB, 3 );
+			} );
+		} );
 	} );
 
 	describe( 'iterator', () => {