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

Commit

Permalink
Merge pull request #326 from ckeditor/i/6319
Browse files Browse the repository at this point in the history
Feature: Added the support for initializing `Collection` items via the `constructor()`. Closes ckeditor/ckeditor5#6319.
  • Loading branch information
oleq authored Mar 23, 2020
2 parents 9c5dff0 + 27fab15 commit 8846e66
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 38 deletions.
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' }
* 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

0 comments on commit 8846e66

Please sign in to comment.