8
0
Просмотр исходного кода

Imported bindTo from ui/viewcollection~ViewCollection with #using() syntax.

Aleksander Nowodzinski 8 лет назад
Родитель
Сommit
b2863c70a8
2 измененных файлов с 314 добавлено и 0 удалено
  1. 143 0
      packages/ckeditor5-utils/src/collection.js
  2. 171 0
      packages/ckeditor5-utils/tests/collection.js

+ 143 - 0
packages/ckeditor5-utils/src/collection.js

@@ -55,6 +55,15 @@ export default class Collection {
 		 * @member {String}
 		 */
 		this._idProperty = options && options.idProperty || 'id';
+
+		/**
+		 * A helper mapping external items from bound collection ({@link #bindTo})
+		 * and actual items of the collection.
+		 *
+		 * @protected
+		 * @member {Map}
+		 */
+		this._boundItemsMap = new Map();
 	}
 
 	/**
@@ -270,6 +279,140 @@ export default class Collection {
 		}
 	}
 
+	/**
+	 * Binds and synchronizes the collection with another one.
+	 *
+	 * The binding can be a simple factory:
+	 *
+	 *		class FactoryClass {
+	 *			constructor( data ) {
+	 *				this.label = data.label;
+	 *			}
+	 *		}
+	 *
+	 *		const source = new Collection( { idProperty: 'label' } );
+	 *		const target = new Collection();
+	 *
+	 *		target.bindTo( source ).using( FactoryClass );
+	 *
+	 *		source.add( { label: 'foo' } );
+	 *		source.add( { label: 'bar' } );
+	 *
+	 *		console.log( target.length ); // 2
+	 *		console.log( target.get( 1 ).label ); // 'bar'
+	 *
+	 *		source.remove( 0 );
+	 *		console.log( target.length ); // 1
+	 *		console.log( target.get( 0 ).label ); // 'bar'
+	 *
+	 * or the factory driven by a custom callback:
+	 *
+	 *		class FooClass {
+	 *			constructor( data ) {
+	 *				this.label = data.label;
+	 *			}
+	 *		}
+	 *
+	 *		class BarClass {
+	 *			constructor( data ) {
+	 *				this.label = data.label;
+	 *			}
+	 *		}
+	 *
+	 *		const source = new Collection( { idProperty: 'label' } );
+	 *		const target = new Collection();
+	 *
+	 *		target.bindTo( source ).using( ( item ) => {
+	 *			if ( item.label == 'foo' ) {
+	 *				return new FooClass( item );
+	 *			} else {
+	 *				return new BarClass( item );
+	 *			}
+	 *		} );
+	 *
+	 *		source.add( { label: 'foo' } );
+	 *		source.add( { label: 'bar' } );
+	 *
+	 *		console.log( target.length ); // 2
+	 *		console.log( target.get( 0 ) instanceof FooClass ); // true
+	 *		console.log( target.get( 1 ) instanceof BarClass ); // true
+	 *
+	 * or the factory out of property name:
+	 *
+	 *		const source = new Collection( { idProperty: 'label' } );
+	 *		const target = new Collection();
+	 *
+	 *		target.bindTo( source ).using( 'label' );
+	 *
+	 *		source.add( { label: { value: 'foo' } } );
+	 *		source.add( { label: { value: 'bar' } } );
+	 *
+	 *		console.log( target.length ); // 2
+	 *		console.log( target.get( 0 ).value ); // 'foo'
+	 *		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}
+	 */
+	bindTo( collection ) {
+		return {
+			/**
+			 * Determines the output of the binding.
+			 *
+			 * @static
+			 * @param {Function|String} CallbackPropertyOrClass When specified as a callback, the callback
+			 * produces the items. When the class constructor is passed, the factory of that class is initialized.
+			 * When the string is provided, the property value is used to create the binding.
+			 */
+			using: ( CallbackPropertyOrClass ) => {
+				let createItem;
+
+				if ( CallbackPropertyOrClass.toString().match( /^class/ ) ) {
+					createItem = ( item ) => {
+						const instance = new CallbackPropertyOrClass( item );
+
+						this._boundItemsMap.set( item, instance );
+
+						return instance;
+					};
+				} else if ( typeof CallbackPropertyOrClass == 'function' ) {
+					createItem = ( item ) => {
+						const instance = CallbackPropertyOrClass( item );
+
+						this._boundItemsMap.set( item, instance );
+
+						return instance;
+					};
+				} else {
+					createItem = ( item ) => {
+						const instance = item[ CallbackPropertyOrClass ];
+
+						this._boundItemsMap.set( item, instance );
+
+						return instance;
+					};
+				}
+
+				// Load the initial content of the collection.
+				for ( let item of collection ) {
+					this.add( createItem( item ) );
+				}
+
+				// Synchronize the with collection as new items are added.
+				this.listenTo( collection, 'add', ( evt, item, index ) => {
+					this.add( createItem( item ), index );
+				} );
+
+				// Synchronize the with collection as new items are removed.
+				this.listenTo( collection, 'remove', ( evt, item ) => {
+					this.remove( this._boundItemsMap.get( item ) );
+
+					this._boundItemsMap.delete( item );
+				} );
+			}
+		};
+	}
+
 	/**
 	 * Collection iterator.
 	 */

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

@@ -512,6 +512,177 @@ describe( 'Collection', () => {
 		} );
 	} );
 
+	describe( 'bindTo()', () => {
+		class FactoryClass {
+			constructor( data ) {
+				this.data = data;
+			}
+		}
+
+		it( 'provides "using()" interface', () => {
+			const returned = collection.bindTo( {} );
+
+			expect( returned ).to.have.keys( 'using' );
+			expect( returned.using ).to.be.a( 'function' );
+		} );
+
+		describe( 'using()', () => {
+			let items;
+
+			beforeEach( () => {
+				items = new Collection();
+			} );
+
+			it( 'does not chain', () => {
+				const returned = collection.bindTo( new Collection() ).using( FactoryClass );
+
+				expect( returned ).to.be.undefined;
+			} );
+
+			describe( 'callback', () => {
+				it( 'creates a binding (arrow function)', () => {
+					collection = new Collection();
+					collection.bindTo( items ).using( ( item ) => {
+						return new FactoryClass( item );
+					} );
+
+					expect( collection ).to.have.length( 0 );
+
+					items.add( { id: '1' } );
+					items.add( { id: '2' } );
+
+					expect( collection ).to.have.length( 2 );
+					expect( collection.get( 0 ) ).to.be.instanceOf( FactoryClass );
+					expect( collection.get( 1 ) ).to.be.instanceOf( FactoryClass );
+					expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) );
+				} );
+
+				// https://github.com/ckeditor/ckeditor5-ui/issues/113
+				it( 'creates a binding (normal function)', () => {
+					collection = new Collection();
+					collection.bindTo( items ).using( function( item ) {
+						return new FactoryClass( item );
+					} );
+
+					items.add( { id: '1' } );
+
+					expect( collection ).to.have.length( 1 );
+
+					const view = collection.get( 0 );
+
+					// Wrong args will be passed to the callback if it's treated as the view constructor.
+					expect( view ).to.be.instanceOf( FactoryClass );
+					expect( view.data ).to.equal( items.get( 0 ) );
+				} );
+
+				it( 'creates a 1:1 binding', () => {
+					collection = new Collection();
+					collection.bindTo( items ).using( item => item );
+
+					expect( collection ).to.have.length( 0 );
+
+					const item1 = { id: '100' };
+					const item2 = { id: '200' };
+
+					items.add( item1 );
+					items.add( item2 );
+
+					expect( collection ).to.have.length( 2 );
+					expect( collection.get( 0 ) ).to.equal( item1 );
+					expect( collection.get( 1 ) ).to.equal( item2 );
+				} );
+			} );
+
+			describe( 'class constructor', () => {
+				it( 'creates a binding (initial content)', () => {
+					items.add( { id: '1' } );
+					items.add( { id: '2' } );
+
+					collection = new Collection();
+					collection.bindTo( items ).using( FactoryClass );
+
+					expect( collection ).to.have.length( 2 );
+					expect( collection.get( 0 ) ).to.be.instanceOf( FactoryClass );
+					expect( collection.get( 1 ) ).to.be.instanceOf( FactoryClass );
+					expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) );
+				} );
+
+				it( 'creates a binding (new content)', () => {
+					collection = new Collection();
+					collection.bindTo( items ).using( FactoryClass );
+
+					expect( collection ).to.have.length( 0 );
+
+					items.add( { id: '1' } );
+					items.add( { id: '2' } );
+
+					expect( collection ).to.have.length( 2 );
+					expect( collection.get( 0 ) ).to.be.instanceOf( FactoryClass );
+					expect( collection.get( 1 ) ).to.be.instanceOf( FactoryClass );
+					expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) );
+				} );
+
+				it( 'creates a binding (item removal)', () => {
+					collection = new Collection();
+					collection.bindTo( items ).using( FactoryClass );
+
+					expect( collection ).to.have.length( 0 );
+
+					items.add( { id: '1' } );
+					items.add( { id: '2' } );
+
+					expect( collection ).to.have.length( 2 );
+					expect( collection.get( 0 ) ).to.be.instanceOf( FactoryClass );
+					expect( collection.get( 1 ) ).to.be.instanceOf( FactoryClass );
+					expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) );
+
+					items.remove( 1 );
+					expect( collection.get( 0 ).data ).to.equal( items.get( 0 ) );
+
+					items.remove( 0 );
+					expect( collection ).to.have.length( 0 );
+				} );
+			} );
+
+			describe( 'property name', () => {
+				it( 'creates a binding', () => {
+					collection = new Collection();
+					collection.bindTo( items ).using( 'prop' );
+
+					expect( collection ).to.have.length( 0 );
+
+					items.add( { prop: { value: 'foo' } } );
+					items.add( { prop: { value: 'bar' } } );
+
+					expect( collection ).to.have.length( 2 );
+					expect( collection.get( 0 ).value ).to.equal( 'foo' );
+					expect( collection.get( 1 ).value ).to.equal( 'bar' );
+				} );
+
+				it( 'creates a binding (item removal)', () => {
+					collection = new Collection();
+					collection.bindTo( items ).using( 'prop' );
+
+					expect( collection ).to.have.length( 0 );
+
+					items.add( { prop: { value: 'foo' } } );
+					items.add( { prop: { value: 'bar' } } );
+
+					expect( collection ).to.have.length( 2 );
+					expect( collection.get( 0 ).value ).to.equal( 'foo' );
+					expect( collection.get( 1 ).value ).to.equal( 'bar' );
+
+					items.remove( 1 );
+					expect( collection ).to.have.length( 1 );
+					expect( collection.get( 0 ).value ).to.equal( 'foo' );
+
+					items.remove( 0 );
+					expect( collection ).to.have.length( 0 );
+				} );
+			} );
+		} );
+	} );
+
 	describe( 'iterator', () => {
 		it( 'covers the whole collection', () => {
 			let item1 = getItem( 'foo' );