ソースを参照

Merge pull request #94 from ckeditor/t/92

t/92: It should be possible to skip some elements on the collection binding
Oskar Wróbel 9 年 前
コミット
00044b4d7f

+ 57 - 54
packages/ckeditor5-ui/src/controllercollection.js

@@ -120,7 +120,9 @@ export default class ControllerCollection extends Collection {
 	 *
 	 *		// Activate the binding – the **factory** is driven by a custom callback.
 	 *		controllers.bind( data ).as( ( item, locale ) => {
-	 *			if ( item.foo == 'bar' ) {
+	 *			if ( !item.foo ) {
+	 *				return null;
+	 *			} else if ( item.foo == 'bar' ) {
 	 *				return new BarController( ..., BarView( locale ) );
 	 *			} else {
 	 *				return new DifferentController( ..., DifferentView( locale ) );
@@ -134,6 +136,9 @@ export default class ControllerCollection extends Collection {
 	 *		// And this one will become DifferentController.
 	 *		data.add( { foo: 'baz' } );
 	 *
+	 *		// Also there will be no controller for data without property `foo`.
+	 *		data.add( {} );
+	 *
 	 *		console.log( controllers.length == 2 );
 	 *
 	 *		// Controller collection is updated as the data is removed.
@@ -151,29 +156,74 @@ export default class ControllerCollection extends Collection {
 	 */
 	bind( collection ) {
 		const controllerMap = new Map();
+		const that = this;
 
 		return {
 			as: ( ControllerClassOrFunction, ViewClass ) => {
-				const factory = ViewClass ?
-						defaultControllerFactory( ControllerClassOrFunction, ViewClass, controllerMap )
+				const handler = ViewClass ?
+						defaultControllerHandler( ControllerClassOrFunction, ViewClass )
 					:
-						genericControllerFactory( ControllerClassOrFunction, controllerMap );
+						genericControllerHandler( ControllerClassOrFunction );
 
 				for ( let item of collection ) {
-					this.add( factory.create( item, this.locale ) );
+					handler.add( item );
 				}
 
 				// Updated controller collection when a new item is added.
 				collection.on( 'add', ( evt, item, index ) => {
-					this.add( factory.create( item, this.locale ), index );
+					handler.add( item, index );
 				} );
 
 				// Update controller collection when a item is removed.
 				collection.on( 'remove', ( evt, item ) => {
-					this.remove( factory.delete( item ) );
+					handler.remove( item );
 				} );
 			}
 		};
+
+		function genericControllerHandler( createController ) {
+			return {
+				add( data, index ) {
+					const controller = createController( data, that.locale );
+
+					controllerMap.set( data, controller );
+
+					if ( controller ) {
+						that.add( controller, index ? recalculateIndex( index ) : undefined );
+					}
+				},
+
+				remove( data ) {
+					const controller = controllerMap.get( data );
+
+					controllerMap.delete( controller );
+
+					if ( controller ) {
+						that.remove( controller );
+					}
+				}
+			};
+		}
+
+		// Decrement index for each item which has no corresponding controller.
+		function recalculateIndex( index ) {
+			let outputIndex = index;
+
+			for ( let i = 0; i < index; i++ ) {
+				// index -> data -> controller
+				if ( !controllerMap.get( collection.get( i ) ) ) {
+					outputIndex--;
+				}
+			}
+
+			return outputIndex;
+		}
+
+		function defaultControllerHandler( ControllerClass, ViewClass ) {
+			return genericControllerHandler( ( model ) => {
+				return new ControllerClass( model, new ViewClass( that.locale ) );
+			}, controllerMap );
+		}
 	}
 
 	/**
@@ -247,53 +297,6 @@ export default class ControllerCollection extends Collection {
 	}
 }
 
-// Initializes a generic controller factory.
-//
-// @param {Function} createController A function which returns controller instance if provided with data and locale.
-// @param {Map} controllerMap A Map used to associate data in the collection with corresponding controller instance.
-// @returns {Object}
-function genericControllerFactory( createController, controllerMap ) {
-	return {
-		// Returns a controller instance (and its view) for given data (i.e. Model) and locale.
-		//
-		// @param {Object} data A data to creates the controller, usually {@link ui.Model}.
-		// @param {utils.Locale} [locale] The {@link ckeditor5.Editor#locale editor's locale} instance.
-		// @returns {ui.Controller}
-		create( data, locale ) {
-			const controller = createController( data, locale );
-
-			controllerMap.set( data, controller );
-
-			return controller;
-		},
-
-		// Deletes controller instance in the factory by associated data and returns
-		// the controller instance.
-		//
-		// @param {Object} data A data associated with the controller, usually {@link ui.Model}.
-		// @returns {ui.Controller}
-		delete( data ) {
-			const controller = controllerMap.get( data );
-
-			controllerMap.delete( controller );
-
-			return controller;
-		}
-	};
-}
-
-// Initializes controller factory with controller and view classes.
-//
-// @param {Function} ControllerClass Specifies the constructor of the controller to be used.
-// @param {Function} ViewClass Specifies constructor of the view.
-// @param {Map} controllerMap A Map used to associate data in the collection with corresponding controller instance.
-// @returns {Object}
-function defaultControllerFactory( ControllerClass, ViewClass, controllerMap ) {
-	return genericControllerFactory( ( model, locale ) => {
-		return new ControllerClass( model, new ViewClass( locale ) );
-	}, controllerMap );
-}
-
 // Check if all entries of the array are of `String` type.
 //
 // @private

+ 67 - 3
packages/ckeditor5-ui/tests/controllercollection.js

@@ -127,7 +127,7 @@ describe( 'ControllerCollection', () => {
 				expect( returned ).to.be.undefined;
 			} );
 
-			describe( 'standard factory', () => {
+			describe( 'standard handler', () => {
 				it( 'expands the initial collection of the models', () => {
 					const controllers = new ControllerCollection( 'synced' );
 
@@ -181,7 +181,7 @@ describe( 'ControllerCollection', () => {
 				} );
 			} );
 
-			describe( 'custom factory', () => {
+			describe( 'custom handler', () => {
 				it( 'expands the initial collection of the models', () => {
 					const controllers = new ControllerCollection( 'synced' );
 
@@ -233,6 +233,70 @@ describe( 'ControllerCollection', () => {
 					expect( controllers.map( c => c.model.uid ) ).to.have.members( [ '0', '1', '3' ] );
 				} );
 
+				it( 'supports returning null', () => {
+					const controllers = new ControllerCollection( 'synced' );
+
+					controllers.bind( models ).as( ( model, locale ) => {
+						if ( model.uid == 2 ) {
+							return null;
+						} else {
+							return new ItemController( model, new ItemView( locale ) );
+						}
+					} );
+
+					expect( controllers.map( c => c.model.uid ) ).to.have.members( [ '0', '1', '3', '4' ] );
+				} );
+
+				it( 'supports adding to structure with missing controller', () => {
+					const controllers = new ControllerCollection( 'synced' );
+
+					controllers.bind( models ).as( ( model, locale ) => {
+						if ( model.uid == 2 ) {
+							return null;
+						} else {
+							return new ItemController( model, new ItemView( locale ) );
+						}
+					} );
+
+					models.add( new Model( { uid: '0.5' } ), 1 );
+					models.add( new Model( { uid: '3.5' } ), 5 );
+
+					expect( models.map( c => c.uid ) ).to.have.members( [ '0', '0.5', '1', '2', '3', '3.5', '4' ] );
+					expect( controllers.map( c => c.model.uid ) ).to.have.members( [ '0', '0.5', '1', '3', '3.5', '4' ] );
+				} );
+
+				it( 'supports removing to structure with missing controller', () => {
+					const controllers = new ControllerCollection( 'synced' );
+
+					controllers.bind( models ).as( ( model, locale ) => {
+						if ( model.uid == 2 ) {
+							return null;
+						} else {
+							return new ItemController( model, new ItemView( locale ) );
+						}
+					} );
+
+					models.remove( 3 );
+
+					expect( controllers.map( c => c.model.uid ) ).to.have.members( [ '0', '1', '4' ] );
+				} );
+
+				it( 'supports removing model with missing controller', () => {
+					const controllers = new ControllerCollection( 'synced' );
+
+					controllers.bind( models ).as( ( model, locale ) => {
+						if ( model.uid == 2 ) {
+							return null;
+						} else {
+							return new ItemController( model, new ItemView( locale ) );
+						}
+					} );
+
+					models.remove( 2 );
+
+					expect( controllers.map( c => c.model.uid ) ).to.have.members( [ '0', '1', '3', '4' ] );
+				} );
+
 				it( 'passes controller collection\'s locale to the views', () => {
 					const locale = {};
 					const controllers = new ControllerCollection( 'synced', locale );
@@ -245,7 +309,7 @@ describe( 'ControllerCollection', () => {
 				} );
 			} );
 
-			describe( 'custom data format with custom factory', () => {
+			describe( 'custom data format with custom handler', () => {
 				it( 'expands the initial collection of the models', () => {
 					const controllers = new ControllerCollection( 'synced' );
 					const data = new Collection();