StructureJS

0.15.2

A class based utility library for building modular and scalable web platform applications. Features opt-in classes and utilities which provide a solid foundation and toolset to build your next project.

File: ts/model/Collection.ts

import BaseModel from '../model/BaseModel';
import EventDispatcher from '../event/EventDispatcher';
import BaseEvent from '../event/BaseEvent';
import Util from '../util/Util';

/**
 * The Collection class provides a way for you to manage your models.
 *
 * @class Collection
 * @extends EventDispatcher
 * @module StructureJS
 * @submodule model
 * @requires Extend
 * @requires EventDispatcher
 * @requires BaseEvent
 * @constructor
 * @param baseModelType {BaseModel} Pass a class that extends BaseModel and the data added to the collection will be created as that type.
 * @author Robert S. (www.codeBelt.com)
 * @example
 *     let data = [{ make: 'Tesla', model: 'Model S', year: 2014 }, { make: 'Tesla', model: 'Model X', year: 2016 }];
 *
 *     // Example of adding data to a collection
 *     let collection = new Collection();
 *     collection.add(data);
 *
 *     // Example of adding data to a collection that will create a CarModel model for each data object passed in.
 *     let collection = new Collection(CarModel);
 *     collection.add(data);
 */
class Collection extends EventDispatcher
{
    /**
     * The list of models in the collection.
     *
     * @property models
     * @type {Array.<any>}
     * @readOnly
     */
    public models:Array<any> = [];

    /**
     * The count of how many models are in the collection.
     *
     * @property length
     * @type {int}
     * @default 0
     * @readOnly
     * @public
     */
    public length:number = 0;

    /**
     * A reference to a BaseModel type that will be used in the collection.
     *
     * @property _modelType
     * @type {any}
     * @protected
     */
    protected _modelType:any = null;

    constructor(baseModelType:any = null)
    {
        super();

        this._modelType = baseModelType;
    }

    /**
     * Adds model or an array of models to the collection.
     *
     * @method add
     * @param model {Any|Array} Single or an array of models to add to the current collection.
     * @param [silent=false] {boolean} If you'd like to prevent the event from being dispatched.
     * @public
     * @chainable
     * @example
     *      collection.add(model);
     *
     *      collection.add([model, model, model, model]);
     *
     *      collection.add(model, true);
     */
    public add(model:any, silent:boolean = false):any
    {
        if (model == null)
        {
            return;
        }

        // If the model passed in is not an array then make it.
        const models:any = (model instanceof Array) ? model : [model];

        const len:number = models.length;
        for (let i:number = 0; i < len; i++)
        {
            // Only add the model if it does not exist in the the collection.
            if (this.has(models[i]) === false)
            {
                if (this._modelType !== null && (models[i] instanceof this._modelType) === false)
                {
                    // If the modelType is set and the data is not already a instance of the modelType
                    // then instantiate it and pass the data into the constructor.
                    this.models.push(new (<any>this)._modelType(models[i]));
                }
                else
                {
                    // Pass the data object to the array.
                    this.models.push(models[i]);
                }

                this.length = this.models.length;
            }
        }

        if (silent === false)
        {
            this.dispatchEvent(new BaseEvent(BaseEvent.ADDED));
        }

        return this;
    }

    /**
     * Removes a model or an array of models from the collection.
     *
     * @method remove
     * @param model {Object|Array} Model(s) to remove
     * @param [silent=false] {boolean} If you'd like to prevent the event from being dispatched.
     * @public
     * @chainable
     * @example
     *      collection.remove(model);
     *
     *      collection.remove([model, model, model, model]);
     *
     *      collection.remove(model, true);
     */
    public remove(model:any, silent:boolean = false):any
    {
        // If the model passed in is not an array then make it.
        const models:any = (model instanceof Array) ? model : [model];

        for (let i:number = models.length - 1; i >= 0; i--)
        {
            // Only remove the model if it exists in the the collection.
            if (this.has(models[i]) === true)
            {
                this.models.splice(this.indexOf(models[i]), 1);
                this.length = this.models.length;
            }
        }

        if (silent === false)
        {
            this.dispatchEvent(new BaseEvent(BaseEvent.REMOVED));
        }

        return this;
    }

    /**
     * Checks if a collection has an model.
     *
     * @method has
     * @param model {Object} Item to check
     * @return {boolean}
     * @public
     * @example
     *      collection.has(model);
     */
    public has(model:any):boolean
    {
        return this.indexOf(model) > -1;
    }

    /**
     * Returns the array index position of the  Base Model.
     *
     * @method indexOf
     * @param model {Object} get the index of.
     * @return {int}
     * @public
     * @example
     *      collection.indexOf(model);
     */
    public indexOf(model:any):number
    {
        return this.models.indexOf(model);
    }

    /**
     * Finds an object by an index value.
     *
     * @method get
     * @param index {int} The index integer of the model to get
     * @return {Object} the model
     * @public
     * @example
     *      let model = collection.get(1);
     */
    public get(index:number):any
    {
        return this.models[index] || null;
    }

    /**
     * Examines each element in a collection, returning an array of all elements that have the given properties.
     * When checking properties, this method performs a deep comparison between values to determine if they are equivalent to each other.
     * @method findBy
     * @param arg {Object|Array}
     * @return {Array.<any>} Returns a list of found object's.
     * @public
     * @example
     *      // Finds all  Base Model that has 'Robert' in it.
     *      collection.findBy("Robert");
     *      // Finds any  Base Model that has 'Robert' or 'Heater' or 23 in it.
     *      collection.findBy(["Robert", "Heather", 32]);
     *
     *      // Finds all  Base Models that same key and value you are searching for.
     *      collection.findBy({ name: 'apple', organic: false, type: 'fruit' });
     *      collection.findBy([{ type: 'vegetable' }, { name: 'apple', 'organic: false, type': 'fruit' }]);
     */
    public findBy(arg:any):Array<any>
    {
        // If properties is not an array then make it an array object.
        const list:Array<any> = (arg instanceof Array) ? arg : [arg];
        let foundItems:Array<any> = [];
        const len:number = list.length;
        let prop:any;

        for (let i:number = 0; i < len; i++)
        {
            prop = list[i];
            // Adds found  Base Model to the foundItems array.
            if ((typeof prop === 'string') || (typeof prop === 'number') || (typeof prop === 'boolean'))
            {
                // If the model is not an object.
                foundItems = foundItems.concat(this._findPropertyValue(prop));
            }
            else
            {
                // If the model is an object.
                foundItems = foundItems.concat(this._where(prop));
            }
        }

        // Removes all duplicated objects found in the temp array.
        return Util.unique(foundItems);
    }

    /**
     * Loops through the models array and creates a new array of models that match all the properties on the object passed in.
     *
     * @method _where
     * @param propList {Object|Array}
     * @return {Array.<any>} Returns a list of found object's.
     * @protected
     */
    protected _where(propList:any):Array<any>
    {
        // If properties is not an array then make it an array object.
        const list:Array<any> = (propList instanceof Array) ? propList : [propList];
        const foundItems:Array<any> = [];
        const itemsLength:number = this.models.length;
        const itemsToFindLength:number = list.length;
        let hasMatchingProperty:boolean = false;
        let doesModelMatch:boolean = false;
        let model:any;
        let obj:any;
        let key:any;
        let j:number;

        for (let i:number = 0; i < itemsToFindLength; i++)
        {
            obj = list[i];

            for (j = 0; j < itemsLength; j++)
            {
                hasMatchingProperty = false;
                doesModelMatch = true;
                model = this.models[j];

                for (key in obj)
                {
                    // Check if the key value is a property.
                    if (obj.hasOwnProperty(key) && model.hasOwnProperty(key))
                    {
                        hasMatchingProperty = true;

                        if (obj[key] !== model[key])
                        {
                            doesModelMatch = false;
                            break;
                        }
                    }
                }

                if (doesModelMatch === true && hasMatchingProperty === true)
                {
                    foundItems.push(model);
                }
            }
        }

        return foundItems;
    }

    /**
     * Loops through all properties of an object and check to see if the value matches the argument passed in.
     *
     * @method _findPropertyValue
     * @param arg {String|Number|Boolean>}
     * @return {Array.<any>} Returns a list of found object's.
     * @protected
     */
    protected _findPropertyValue(arg):Array<any>
    {
        // If properties is not an array then make it an array object.
        const list = (arg instanceof Array) ? arg : [arg];
        const foundItems:Array<any> = [];
        const itemsLength:number = this.models.length;
        const itemsToFindLength:number = list.length;
        let propertyValue:any;
        let value:any;
        let model:any;
        let key:any;
        let j:any;

        for (let i:number = 0; i < itemsLength; i++)
        {
            model = this.models[i];

            for (key in model)
            {
                // Check if the key value is a property.
                if (model.hasOwnProperty(key))
                {
                    propertyValue = model[key];

                    for (j = 0; j < itemsToFindLength; j++)
                    {
                        value = list[j];

                        // If the  Base Model property equals the string value then keep a reference to that  Base Model.
                        if (propertyValue === value)
                        {
                            // Add found  Base Model to the foundItems array.
                            foundItems.push(model);
                            break;
                        }
                    }
                }
            }
        }

        return foundItems;
    }

    /**
     * Clears or remove all the models from the collection.
     *
     * @method clear
     * @param [silent=false] {boolean} If you'd like to prevent the event from being dispatched.
     * @public
     * @chainable
     * @example
     *      collection.clear();
     */
    public clear(silent:boolean = false):any
    {
        this.models = [];
        this.length = 0;

        if (silent === false)
        {
            this.dispatchEvent(new BaseEvent(BaseEvent.CLEAR));
        }

        return this;
    }

    /**
     * Creates and returns a new collection object that contains a reference to the models in the collection cloned from.
     *
     * @method clone
     * @returns {Collection}
     * @public
     * @example
     *     let clone = collection.clone();
     */
    public clone():Collection
    {
        const clonedBaseModel:Collection = new (<any>this).constructor(this._modelType);
        clonedBaseModel.add(this.models.slice(0));

        return clonedBaseModel;
    }

    /**
     * Creates a JSON object of the collection.
     *
     * @method toJSON
     * @returns {Array.<any>}
     * @public
     * @example
     *     let arrayOfObjects = collection.toJSON();
     */
    public toJSON():Array<any>
    {
        if (this._modelType !== null)
        {
            const list:Array<any> = [];
            const len:number = this.length;

            for (let i:number = 0; i < len; i++)
            {
                list[i] = this.models[i].toJSON();
            }

            return list;
        }
        else
        {
            return Util.clone(this.models);
        }
    }

    /**
     * Creates a JSON string of the collection.
     *
     * @method toJSONString
     * @returns {string}
     * @public
     * @example
     *     let str = collection.toJSONString();
     */
    public toJSONString():string
    {
        return JSON.stringify(this.toJSON());
    }

    /**
     * Converts the string json data into an Objects and calls the {{#crossLink "Collection/add:method"}}{{/crossLink}} method to add the objects to the collection.
     *
     * @method fromJSON
     * @param json {string}
     * @public
     * @chainable
     * @example
     *      collection.fromJSON(str);
     */
    public fromJSON(json):any
    {
        const parsedData:any = JSON.parse(json);

        this.add(parsedData);

        return this;
    }

    /**
     * Allows you to sort models that have one or more common properties, specifying the property or properties to use as the sort keys
     *
     * @method sortOn
     * @param propertyName {string}
     * @param [sortAscending=true] {boolean}
     * @public
     * @return {Array<any>} Returns the list of models in the collection.
     * @example
     *      collection.sortOn('name');
     *      collection.sortOn('name', false);
     */
    public sortOn(propertyName:string, sortAscending:boolean = true):Array<any>
    {
        if (sortAscending === false)
        {
            return this.sort(function (a, b)
            {
                if (a[propertyName] < b[propertyName])
                {
                    return 1;
                }

                if (a[propertyName] > b[propertyName])
                {
                    return -1;
                }

                return 0;
            });
        }
        else
        {
            return this.sort(function (a, b)
            {
                if (a[propertyName] > b[propertyName])
                {
                    return 1;
                }

                if (a[propertyName] < b[propertyName])
                {
                    return -1;
                }

                return 0;
            });
        }
    }

    /**
     * Specifies a function that defines the sort order. If omitted, the array is sorted according to each character's Unicode code
     * point value, according to the string conversion of each element.
     *
     * @method sort
     * @param [sortFunction=null] {Function}
     * @public
     * @return {Array.<any>} Returns the list of models in the collection.
     * @example
     *      let sortByDate = function(a, b){
     *          return new Date(a.date) - new Date(b.date)
     *      }
     *
     *      collection.sort(sortByDate);
     */
    public sort(sortFunction = null):Array<any>
    {
        this.models.sort(sortFunction);

        return this.models;
    }

    /**
     * The filter method creates a new array with all elements that pass the test implemented by the provided function.
     *
     * @method filter
     * @param callback {Function} Function to test each element of the array. Invoked with arguments (element, index, array). Return true to keep the element, false otherwise.
     * @param [callbackScope=null] Optional. Value to use as this when executing callback.
     * @public
     * @return {Array.<any>} Returns the list of models in the collection.
     * @example
     *      let isOldEnough = function(model){
     *          return model.age >= 21;
     *      }
     *
     *      let list = collection.filter(isOldEnough);
     */
    public filter(callback:any, callbackScope:any = null):Array<any>
    {
        return this.models.filter(callback, callbackScope);
    }

    /**
     * Convenient way to get a list of property values.
     *
     * @method pluck
     * @param propertyName {string} The property name you want the values from.
     * @param [unique=false] {string} Pass in true to remove duplicates.
     * @return {Array.<any>}
     * @public
     * @example
     *      collection.add([{name: 'Robert'}, {name: 'Robert'}, {name: 'Chris'}]);
     *
     *      let list = collection.pluck('name');
     *      // ['Robert', 'Robert', 'Chris']
     *
     *      let list = collection.pluck('name', true);
     *      // ['Robert', 'Chris']
     */
    public pluck(propertyName:string, unique:boolean = false):Array<any>
    {
        let list:Array<any> = [];

        for (let i = 0; i < this.length; i++) {
            if (this.models[i].hasOwnProperty(propertyName) === true) {
                list[i] = this.models[i][propertyName];
            }
        }

        if (unique === true) {
            list = Util.unique(list);
        }

        return list;
    }

    /**
     * Convenient way to group models into categories/groups by a property name.
     *
     * @method groupBy
     * @param propertyName {string} The string value of the property you want to group with.
     * @return {any} Returns an object that is categorized by the property name.
     * @public
     * @example
     *      collection.add([{name: 'Robert', id: 0}, {name: 'Robert', id: 1}, {name: 'Chris', id: 2}]);
     *
     *      let list = collection.groupBy('name');
     *
     *      // {
     *      //    Robert: [{name: 'Robert', id: 0}, {name: 'Robert', id: 1}]
     *      //    Chris: [{name: 'Chris', id: 2}]
     *      // }
     */
    public groupBy(propertyName):any
    {
        let model:any;
        let groupName:string;
        let groupList:any = {};

        // Loop through all the models in this collection.
        for (let i:number = 0; i < this.length; i++) {
            model = this.models[i];
            // Get the value from the property name passed in and uses that as the group name.
            groupName = model[propertyName];

            if (groupList[groupName] == null) {
                groupList[groupName] = [];
            }
            groupList[groupName].push(model);
        }
        return groupList;
    }

    /**
     * Changes the order of the models so that the last model becomes the first model, the penultimate model becomes the second, and so on.
     *
     * @method reverse
     * @public
     * @return {Array.<any>} Returns the list of models in the collection.
     * @example
     *      collection.reverse();
     */
    public reverse():Array<any>
    {
        return this.models.reverse();
    }

}

export default Collection;