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

Added support for initializing Collection using a constructor #326

Merged
merged 14 commits into from
Mar 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 89 additions & 29 deletions src/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import EmitterMixin from './emittermixin';
import CKEditorError from './ckeditorerror';
import uid from './uid';
import isIterable from './isiterable';
import mix from './mix';

/**
Expand All @@ -28,10 +29,46 @@ export default class Collection {
/**
* Creates a new Collection instance.
*
* @param {Object} [options={}] The options object.
* @param {String} [options.idProperty='id'] The name of the property which is considered to identify an item.
* You can provide an iterable of initial items the collection will be created with:
*
* const collection = new Collection( [ { id: 'John' }, { id: 'Mike' } ] );
*
* console.log( collection.get( 0 ) ); // -> { id: 'John' }
* console.log( collection.get( 1 ) ); // -> { id: 'Mike' }
* console.log( collection.get( 'Mike' ) ); // -> { id: 'Mike' }
*
* Or you can first create a collection and then add new items using the {@link #add} method:
*
* const collection = new Collection();
*
* collection.add( { id: 'John' } );
* console.log( collection.get( 0 ) ); // -> { id: 'John' }
*
* Whatever option you choose, you can always pass a configuration object as the last argument
* of the constructor:
*
* const emptyCollection = new Collection( { idProperty: 'name' } );
* emptyCollection.add( { name: 'John' } );
* console.log( collection.get( 'John' ) ); // -> { name: 'John' }
*
* const nonEmptyCollection = new Collection( [ { name: 'John' } ], { idProperty: 'name' } );
* nonEmptyCollection.add( { name: 'George' } );
* console.log( collection.get( 'George' ) ); // -> { name: 'George' }
mlewand marked this conversation as resolved.
Show resolved Hide resolved
* console.log( collection.get( 'John' ) ); // -> { name: 'John' }
*
* @param {Iterable.<Object>|Object} initialItemsOrOptions The initial items of the collection or
* the options object.
* @param {Object} [options={}] The options object, when the first argument is an array of initial items.
* @param {String} [options.idProperty='id'] The name of the property which is used to identify an item.
* Items that do not have such a property will be assigned one when added to the collection.
*/
constructor( options = {} ) {
constructor( initialItemsOrOptions = {}, options = {} ) {
const hasInitialItems = isIterable( initialItemsOrOptions );

if ( !hasInitialItems ) {
options = initialItemsOrOptions;
}

/**
* The internal list of items in the collection.
*
Expand Down Expand Up @@ -88,6 +125,14 @@ export default class Collection {
*/
this._skippedIndexesFromExternal = [];

// Set the initial content of the collection (if provided in the constructor).
if ( hasInitialItems ) {
for ( const item of initialItemsOrOptions ) {
this._items.push( item );
this._itemMap.set( this._getItemIdBeforeAdding( item ), item );
}
}

/**
* A collection instance this collection is bound to as a result
* of calling {@link #bindTo} method.
Expand Down Expand Up @@ -136,32 +181,7 @@ export default class Collection {
* @fires add
*/
add( item, index ) {
let itemId;
const idProperty = this._idProperty;

if ( ( idProperty in item ) ) {
itemId = item[ idProperty ];

if ( typeof itemId != 'string' ) {
/**
* This item's id should be a string.
*
* @error collection-add-invalid-id
*/
throw new CKEditorError( 'collection-add-invalid-id', this );
}

if ( this.get( itemId ) ) {
/**
* This item already exists in the collection.
*
* @error collection-add-item-already-exists
*/
throw new CKEditorError( 'collection-add-item-already-exists', this );
}
} else {
item[ idProperty ] = itemId = uid();
}
const itemId = this._getItemIdBeforeAdding( item );

// TODO: Use ES6 default function argument.
if ( index === undefined ) {
Expand Down Expand Up @@ -604,6 +624,46 @@ export default class Collection {
} );
}

/**
* Returns an unique id property for a given `item`.
*
* The method will generate new id and assign it to the `item` if it doesn't have any.
*
* @private
* @param {Object} item Item to be added.
* @returns {String}
*/
_getItemIdBeforeAdding( item ) {
const idProperty = this._idProperty;
let itemId;

if ( ( idProperty in item ) ) {
itemId = item[ idProperty ];

if ( typeof itemId != 'string' ) {
/**
* This item's id should be a string.
*
* @error collection-add-invalid-id
*/
throw new CKEditorError( 'collection-add-invalid-id', this );
}

if ( this.get( itemId ) ) {
/**
* This item already exists in the collection.
*
* @error collection-add-item-already-exists
*/
throw new CKEditorError( 'collection-add-item-already-exists', this );
}
} else {
item[ idProperty ] = itemId = uid();
}

return itemId;
}

/**
* Iterable interface.
*
Expand Down
83 changes: 74 additions & 9 deletions tests/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,83 @@ describe( 'Collection', () => {
} );

describe( 'constructor()', () => {
it( 'allows to change the id property used by the collection', () => {
const item1 = { id: 'foo', name: 'xx' };
const item2 = { id: 'foo', name: 'yy' };
const collection = new Collection( { idProperty: 'name' } );
describe( 'setting initial collection items', () => {
it( 'should work using an array', () => {
const item1 = getItem( 'foo' );
const item2 = getItem( 'bar' );
const collection = new Collection( [ item1, item2 ] );

collection.add( item1 );
collection.add( item2 );
expect( collection ).to.have.length( 2 );

expect( collection ).to.have.length( 2 );
expect( collection.get( 0 ) ).to.equal( item1 );
expect( collection.get( 1 ) ).to.equal( item2 );
expect( collection.get( 'foo' ) ).to.equal( item1 );
expect( collection.get( 'bar' ) ).to.equal( item2 );
} );

it( 'should work using an iterable', () => {
const item1 = getItem( 'foo' );
const item2 = getItem( 'bar' );
const itemsSet = new Set( [ item1, item2 ] );
const collection = new Collection( itemsSet );

expect( collection ).to.have.length( 2 );

expect( collection.get( 0 ) ).to.equal( item1 );
expect( collection.get( 1 ) ).to.equal( item2 );
} );

it( 'should generate ids for items that doesn\'t have it', () => {
const item = {};
const collection = new Collection( [ item ] );

expect( collection.get( 0 ).id ).to.be.a( 'string' );
expect( collection.get( 0 ).id ).not.to.be.empty;
} );

it( 'should throw an error when an invalid item key is provided', () => {
const badIdItem = getItem( 1 ); // Number id is not supported.

expectToThrowCKEditorError( () => {
return new Collection( [ badIdItem ] );
}, /^collection-add-invalid-id/ );
} );

it( 'should throw an error when two items have the same key', () => {
const item1 = getItem( 'foo' );
const item2 = getItem( 'foo' );

expectToThrowCKEditorError( () => {
return new Collection( [ item1, item2 ] );
}, /^collection-add-item-already-exists/ );
} );
} );

describe( 'options', () => {
it( 'should allow to change the id property used by the collection', () => {
const item1 = { id: 'foo', name: 'xx' };
const item2 = { id: 'foo', name: 'yy' };
const collection = new Collection( { idProperty: 'name' } );

collection.add( item1 );
collection.add( item2 );

expect( collection.get( 'xx' ) ).to.equal( item1 );
expect( collection.remove( 'yy' ) ).to.equal( item2 );
expect( collection ).to.have.length( 2 );

expect( collection.get( 'xx' ) ).to.equal( item1 );
expect( collection.remove( 'yy' ) ).to.equal( item2 );
} );

it( 'should allow to change the id property used by the collection (initial items passed to the constructor)', () => {
const item1 = { id: 'foo', name: 'xx' };
const item2 = { id: 'foo', name: 'yy' };
const collection = new Collection( [ item1, item2 ], { idProperty: 'name' } );

expect( collection ).to.have.length( 2 );

expect( collection.get( 'xx' ) ).to.equal( item1 );
expect( collection.remove( 'yy' ) ).to.equal( item2 );
} );
} );
} );

Expand Down