@@ -55,6 +55,15 @@ export default class Collection {
5555 * @member {String}
5656 */
5757 this . _idProperty = options && options . idProperty || 'id' ;
58+
59+ /**
60+ * A helper mapping external items from bound collection ({@link #bindTo})
61+ * and actual items of the collection.
62+ *
63+ * @protected
64+ * @member {Map}
65+ */
66+ this . _boundItemsMap = new Map ( ) ;
5867 }
5968
6069 /**
@@ -270,6 +279,156 @@ export default class Collection {
270279 }
271280 }
272281
282+ /**
283+ * Binds and synchronizes the collection with another one.
284+ *
285+ * The binding can be a simple factory:
286+ *
287+ * class FactoryClass {
288+ * constructor( data ) {
289+ * this.label = data.label;
290+ * }
291+ * }
292+ *
293+ * const source = new Collection( { idProperty: 'label' } );
294+ * const target = new Collection();
295+ *
296+ * target.bindTo( source ).as( FactoryClass );
297+ *
298+ * source.add( { label: 'foo' } );
299+ * source.add( { label: 'bar' } );
300+ *
301+ * console.log( target.length ); // 2
302+ * console.log( target.get( 1 ).label ); // 'bar'
303+ *
304+ * source.remove( 0 );
305+ * console.log( target.length ); // 1
306+ * console.log( target.get( 0 ).label ); // 'bar'
307+ *
308+ * or the factory driven by a custom callback:
309+ *
310+ * class FooClass {
311+ * constructor( data ) {
312+ * this.label = data.label;
313+ * }
314+ * }
315+ *
316+ * class BarClass {
317+ * constructor( data ) {
318+ * this.label = data.label;
319+ * }
320+ * }
321+ *
322+ * const source = new Collection( { idProperty: 'label' } );
323+ * const target = new Collection();
324+ *
325+ * target.bindTo( source ).using( ( item ) => {
326+ * if ( item.label == 'foo' ) {
327+ * return new FooClass( item );
328+ * } else {
329+ * return new BarClass( item );
330+ * }
331+ * } );
332+ *
333+ * source.add( { label: 'foo' } );
334+ * source.add( { label: 'bar' } );
335+ *
336+ * console.log( target.length ); // 2
337+ * console.log( target.get( 0 ) instanceof FooClass ); // true
338+ * console.log( target.get( 1 ) instanceof BarClass ); // true
339+ *
340+ * or the factory out of property name:
341+ *
342+ * const source = new Collection( { idProperty: 'label' } );
343+ * const target = new Collection();
344+ *
345+ * target.bindTo( source ).using( 'label' );
346+ *
347+ * source.add( { label: { value: 'foo' } } );
348+ * source.add( { label: { value: 'bar' } } );
349+ *
350+ * console.log( target.length ); // 2
351+ * console.log( target.get( 0 ).value ); // 'foo'
352+ * console.log( target.get( 1 ).value ); // 'bar'
353+ *
354+ * @param {module:utils/collection~Collection } collection A collection to be bound.
355+ * @returns {module:ui/viewcollection~ViewCollection#bindTo#using }
356+ */
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 ) ) ;
376+
377+ this . _boundItemsMap . delete ( item ) ;
378+ } ) ;
379+ } ;
380+
381+ return {
382+ /**
383+ * Creates the class factory binding.
384+ *
385+ * @static
386+ * @param {Function } Class Specifies which class factory is to be initialized.
387+ */
388+ as : ( Class ) => {
389+ bind ( ( item ) => {
390+ const instance = new Class ( item ) ;
391+
392+ this . _boundItemsMap . set ( item , instance ) ;
393+
394+ return instance ;
395+ } ) ;
396+ } ,
397+
398+ /**
399+ * Creates callback or property binding.
400+ *
401+ * @static
402+ * @param {Function|String } callbackOrProperty When the function is passed, it is used to
403+ * produce the items. When the string is provided, the property value is used to create
404+ * the bound collection items.
405+ */
406+ using : ( callbackOrProperty ) => {
407+ let factory ;
408+
409+ if ( typeof callbackOrProperty == 'function' ) {
410+ factory = ( item ) => {
411+ const instance = callbackOrProperty ( item ) ;
412+
413+ this . _boundItemsMap . set ( item , instance ) ;
414+
415+ return instance ;
416+ } ;
417+ } else {
418+ factory = ( item ) => {
419+ const instance = item [ callbackOrProperty ] ;
420+
421+ this . _boundItemsMap . set ( item , instance ) ;
422+
423+ return instance ;
424+ } ;
425+ }
426+
427+ bind ( factory ) ;
428+ }
429+ } ;
430+ }
431+
273432 /**
274433 * Collection iterator.
275434 */
0 commit comments