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

Commit 4e299be

Browse files
author
Piotr Jasiun
authored
Merge pull request #131 from ckeditor/t/125
Feature: `Collectio.bindTo` method now is not only available in the `ViewCollection` but in all `Collection`s. Closes #125.
2 parents 8d33b89 + 9f1d6c9 commit 4e299be

File tree

2 files changed

+386
-9
lines changed

2 files changed

+386
-9
lines changed

src/collection.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)