ts/model/Collection.ts - StructureJS

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

  1. import BaseModel from '../model/BaseModel';
  2. import EventDispatcher from '../event/EventDispatcher';
  3. import BaseEvent from '../event/BaseEvent';
  4. import Util from '../util/Util';
  5.  
  6. /**
  7. * The Collection class provides a way for you to manage your models.
  8. *
  9. * @class Collection
  10. * @extends EventDispatcher
  11. * @module StructureJS
  12. * @submodule model
  13. * @requires Extend
  14. * @requires EventDispatcher
  15. * @requires BaseEvent
  16. * @constructor
  17. * @param baseModelType {BaseModel} Pass a class that extends BaseModel and the data added to the collection will be created as that type.
  18. * @author Robert S. (www.codeBelt.com)
  19. * @example
  20. * let data = [{ make: 'Tesla', model: 'Model S', year: 2014 }, { make: 'Tesla', model: 'Model X', year: 2016 }];
  21. *
  22. * // Example of adding data to a collection
  23. * let collection = new Collection();
  24. * collection.add(data);
  25. *
  26. * // Example of adding data to a collection that will create a CarModel model for each data object passed in.
  27. * let collection = new Collection(CarModel);
  28. * collection.add(data);
  29. */
  30. class Collection extends EventDispatcher
  31. {
  32. /**
  33. * The list of models in the collection.
  34. *
  35. * @property models
  36. * @type {Array.<any>}
  37. * @readOnly
  38. */
  39. public models:Array<any> = [];
  40.  
  41. /**
  42. * The count of how many models are in the collection.
  43. *
  44. * @property length
  45. * @type {int}
  46. * @default 0
  47. * @readOnly
  48. * @public
  49. */
  50. public length:number = 0;
  51.  
  52. /**
  53. * A reference to a BaseModel type that will be used in the collection.
  54. *
  55. * @property _modelType
  56. * @type {any}
  57. * @protected
  58. */
  59. protected _modelType:any = null;
  60.  
  61. constructor(baseModelType:any = null)
  62. {
  63. super();
  64.  
  65. this._modelType = baseModelType;
  66. }
  67.  
  68. /**
  69. * Adds model or an array of models to the collection.
  70. *
  71. * @method add
  72. * @param model {Any|Array} Single or an array of models to add to the current collection.
  73. * @param [silent=false] {boolean} If you'd like to prevent the event from being dispatched.
  74. * @public
  75. * @chainable
  76. * @example
  77. * collection.add(model);
  78. *
  79. * collection.add([model, model, model, model]);
  80. *
  81. * collection.add(model, true);
  82. */
  83. public add(model:any, silent:boolean = false):any
  84. {
  85. if (model == null)
  86. {
  87. return;
  88. }
  89.  
  90. // If the model passed in is not an array then make it.
  91. const models:any = (model instanceof Array) ? model : [model];
  92.  
  93. const len:number = models.length;
  94. for (let i:number = 0; i < len; i++)
  95. {
  96. // Only add the model if it does not exist in the the collection.
  97. if (this.has(models[i]) === false)
  98. {
  99. if (this._modelType !== null && (models[i] instanceof this._modelType) === false)
  100. {
  101. // If the modelType is set and the data is not already a instance of the modelType
  102. // then instantiate it and pass the data into the constructor.
  103. this.models.push(new (<any>this)._modelType(models[i]));
  104. }
  105. else
  106. {
  107. // Pass the data object to the array.
  108. this.models.push(models[i]);
  109. }
  110.  
  111. this.length = this.models.length;
  112. }
  113. }
  114.  
  115. if (silent === false)
  116. {
  117. this.dispatchEvent(new BaseEvent(BaseEvent.ADDED));
  118. }
  119.  
  120. return this;
  121. }
  122.  
  123. /**
  124. * Removes a model or an array of models from the collection.
  125. *
  126. * @method remove
  127. * @param model {Object|Array} Model(s) to remove
  128. * @param [silent=false] {boolean} If you'd like to prevent the event from being dispatched.
  129. * @public
  130. * @chainable
  131. * @example
  132. * collection.remove(model);
  133. *
  134. * collection.remove([model, model, model, model]);
  135. *
  136. * collection.remove(model, true);
  137. */
  138. public remove(model:any, silent:boolean = false):any
  139. {
  140. // If the model passed in is not an array then make it.
  141. const models:any = (model instanceof Array) ? model : [model];
  142.  
  143. for (let i:number = models.length - 1; i >= 0; i--)
  144. {
  145. // Only remove the model if it exists in the the collection.
  146. if (this.has(models[i]) === true)
  147. {
  148. this.models.splice(this.indexOf(models[i]), 1);
  149. this.length = this.models.length;
  150. }
  151. }
  152.  
  153. if (silent === false)
  154. {
  155. this.dispatchEvent(new BaseEvent(BaseEvent.REMOVED));
  156. }
  157.  
  158. return this;
  159. }
  160.  
  161. /**
  162. * Checks if a collection has an model.
  163. *
  164. * @method has
  165. * @param model {Object} Item to check
  166. * @return {boolean}
  167. * @public
  168. * @example
  169. * collection.has(model);
  170. */
  171. public has(model:any):boolean
  172. {
  173. return this.indexOf(model) > -1;
  174. }
  175.  
  176. /**
  177. * Returns the array index position of the Base Model.
  178. *
  179. * @method indexOf
  180. * @param model {Object} get the index of.
  181. * @return {int}
  182. * @public
  183. * @example
  184. * collection.indexOf(model);
  185. */
  186. public indexOf(model:any):number
  187. {
  188. return this.models.indexOf(model);
  189. }
  190.  
  191. /**
  192. * Finds an object by an index value.
  193. *
  194. * @method get
  195. * @param index {int} The index integer of the model to get
  196. * @return {Object} the model
  197. * @public
  198. * @example
  199. * let model = collection.get(1);
  200. */
  201. public get(index:number):any
  202. {
  203. return this.models[index] || null;
  204. }
  205.  
  206. /**
  207. * Examines each element in a collection, returning an array of all elements that have the given properties.
  208. * When checking properties, this method performs a deep comparison between values to determine if they are equivalent to each other.
  209. * @method findBy
  210. * @param arg {Object|Array}
  211. * @return {Array.<any>} Returns a list of found object's.
  212. * @public
  213. * @example
  214. * // Finds all Base Model that has 'Robert' in it.
  215. * collection.findBy("Robert");
  216. * // Finds any Base Model that has 'Robert' or 'Heater' or 23 in it.
  217. * collection.findBy(["Robert", "Heather", 32]);
  218. *
  219. * // Finds all Base Models that same key and value you are searching for.
  220. * collection.findBy({ name: 'apple', organic: false, type: 'fruit' });
  221. * collection.findBy([{ type: 'vegetable' }, { name: 'apple', 'organic: false, type': 'fruit' }]);
  222. */
  223. public findBy(arg:any):Array<any>
  224. {
  225. // If properties is not an array then make it an array object.
  226. const list:Array<any> = (arg instanceof Array) ? arg : [arg];
  227. let foundItems:Array<any> = [];
  228. const len:number = list.length;
  229. let prop:any;
  230.  
  231. for (let i:number = 0; i < len; i++)
  232. {
  233. prop = list[i];
  234. // Adds found Base Model to the foundItems array.
  235. if ((typeof prop === 'string') || (typeof prop === 'number') || (typeof prop === 'boolean'))
  236. {
  237. // If the model is not an object.
  238. foundItems = foundItems.concat(this._findPropertyValue(prop));
  239. }
  240. else
  241. {
  242. // If the model is an object.
  243. foundItems = foundItems.concat(this._where(prop));
  244. }
  245. }
  246.  
  247. // Removes all duplicated objects found in the temp array.
  248. return Util.unique(foundItems);
  249. }
  250.  
  251. /**
  252. * Loops through the models array and creates a new array of models that match all the properties on the object passed in.
  253. *
  254. * @method _where
  255. * @param propList {Object|Array}
  256. * @return {Array.<any>} Returns a list of found object's.
  257. * @protected
  258. */
  259. protected _where(propList:any):Array<any>
  260. {
  261. // If properties is not an array then make it an array object.
  262. const list:Array<any> = (propList instanceof Array) ? propList : [propList];
  263. const foundItems:Array<any> = [];
  264. const itemsLength:number = this.models.length;
  265. const itemsToFindLength:number = list.length;
  266. let hasMatchingProperty:boolean = false;
  267. let doesModelMatch:boolean = false;
  268. let model:any;
  269. let obj:any;
  270. let key:any;
  271. let j:number;
  272.  
  273. for (let i:number = 0; i < itemsToFindLength; i++)
  274. {
  275. obj = list[i];
  276.  
  277. for (j = 0; j < itemsLength; j++)
  278. {
  279. hasMatchingProperty = false;
  280. doesModelMatch = true;
  281. model = this.models[j];
  282.  
  283. for (key in obj)
  284. {
  285. // Check if the key value is a property.
  286. if (obj.hasOwnProperty(key) && model.hasOwnProperty(key))
  287. {
  288. hasMatchingProperty = true;
  289.  
  290. if (obj[key] !== model[key])
  291. {
  292. doesModelMatch = false;
  293. break;
  294. }
  295. }
  296. }
  297.  
  298. if (doesModelMatch === true && hasMatchingProperty === true)
  299. {
  300. foundItems.push(model);
  301. }
  302. }
  303. }
  304.  
  305. return foundItems;
  306. }
  307.  
  308. /**
  309. * Loops through all properties of an object and check to see if the value matches the argument passed in.
  310. *
  311. * @method _findPropertyValue
  312. * @param arg {String|Number|Boolean>}
  313. * @return {Array.<any>} Returns a list of found object's.
  314. * @protected
  315. */
  316. protected _findPropertyValue(arg):Array<any>
  317. {
  318. // If properties is not an array then make it an array object.
  319. const list = (arg instanceof Array) ? arg : [arg];
  320. const foundItems:Array<any> = [];
  321. const itemsLength:number = this.models.length;
  322. const itemsToFindLength:number = list.length;
  323. let propertyValue:any;
  324. let value:any;
  325. let model:any;
  326. let key:any;
  327. let j:any;
  328.  
  329. for (let i:number = 0; i < itemsLength; i++)
  330. {
  331. model = this.models[i];
  332.  
  333. for (key in model)
  334. {
  335. // Check if the key value is a property.
  336. if (model.hasOwnProperty(key))
  337. {
  338. propertyValue = model[key];
  339.  
  340. for (j = 0; j < itemsToFindLength; j++)
  341. {
  342. value = list[j];
  343.  
  344. // If the Base Model property equals the string value then keep a reference to that Base Model.
  345. if (propertyValue === value)
  346. {
  347. // Add found Base Model to the foundItems array.
  348. foundItems.push(model);
  349. break;
  350. }
  351. }
  352. }
  353. }
  354. }
  355.  
  356. return foundItems;
  357. }
  358.  
  359. /**
  360. * Clears or remove all the models from the collection.
  361. *
  362. * @method clear
  363. * @param [silent=false] {boolean} If you'd like to prevent the event from being dispatched.
  364. * @public
  365. * @chainable
  366. * @example
  367. * collection.clear();
  368. */
  369. public clear(silent:boolean = false):any
  370. {
  371. this.models = [];
  372. this.length = 0;
  373.  
  374. if (silent === false)
  375. {
  376. this.dispatchEvent(new BaseEvent(BaseEvent.CLEAR));
  377. }
  378.  
  379. return this;
  380. }
  381.  
  382. /**
  383. * Creates and returns a new collection object that contains a reference to the models in the collection cloned from.
  384. *
  385. * @method clone
  386. * @returns {Collection}
  387. * @public
  388. * @example
  389. * let clone = collection.clone();
  390. */
  391. public clone():Collection
  392. {
  393. const clonedBaseModel:Collection = new (<any>this).constructor(this._modelType);
  394. clonedBaseModel.add(this.models.slice(0));
  395.  
  396. return clonedBaseModel;
  397. }
  398.  
  399. /**
  400. * Creates a JSON object of the collection.
  401. *
  402. * @method toJSON
  403. * @returns {Array.<any>}
  404. * @public
  405. * @example
  406. * let arrayOfObjects = collection.toJSON();
  407. */
  408. public toJSON():Array<any>
  409. {
  410. if (this._modelType !== null)
  411. {
  412. const list:Array<any> = [];
  413. const len:number = this.length;
  414.  
  415. for (let i:number = 0; i < len; i++)
  416. {
  417. list[i] = this.models[i].toJSON();
  418. }
  419.  
  420. return list;
  421. }
  422. else
  423. {
  424. return Util.clone(this.models);
  425. }
  426. }
  427.  
  428. /**
  429. * Creates a JSON string of the collection.
  430. *
  431. * @method toJSONString
  432. * @returns {string}
  433. * @public
  434. * @example
  435. * let str = collection.toJSONString();
  436. */
  437. public toJSONString():string
  438. {
  439. return JSON.stringify(this.toJSON());
  440. }
  441.  
  442. /**
  443. * Converts the string json data into an Objects and calls the {{#crossLink "Collection/add:method"}}{{/crossLink}} method to add the objects to the collection.
  444. *
  445. * @method fromJSON
  446. * @param json {string}
  447. * @public
  448. * @chainable
  449. * @example
  450. * collection.fromJSON(str);
  451. */
  452. public fromJSON(json):any
  453. {
  454. const parsedData:any = JSON.parse(json);
  455.  
  456. this.add(parsedData);
  457.  
  458. return this;
  459. }
  460.  
  461. /**
  462. * Allows you to sort models that have one or more common properties, specifying the property or properties to use as the sort keys
  463. *
  464. * @method sortOn
  465. * @param propertyName {string}
  466. * @param [sortAscending=true] {boolean}
  467. * @public
  468. * @return {Array<any>} Returns the list of models in the collection.
  469. * @example
  470. * collection.sortOn('name');
  471. * collection.sortOn('name', false);
  472. */
  473. public sortOn(propertyName:string, sortAscending:boolean = true):Array<any>
  474. {
  475. if (sortAscending === false)
  476. {
  477. return this.sort(function (a, b)
  478. {
  479. if (a[propertyName] < b[propertyName])
  480. {
  481. return 1;
  482. }
  483.  
  484. if (a[propertyName] > b[propertyName])
  485. {
  486. return -1;
  487. }
  488.  
  489. return 0;
  490. });
  491. }
  492. else
  493. {
  494. return this.sort(function (a, b)
  495. {
  496. if (a[propertyName] > b[propertyName])
  497. {
  498. return 1;
  499. }
  500.  
  501. if (a[propertyName] < b[propertyName])
  502. {
  503. return -1;
  504. }
  505.  
  506. return 0;
  507. });
  508. }
  509. }
  510.  
  511. /**
  512. * Specifies a function that defines the sort order. If omitted, the array is sorted according to each character's Unicode code
  513. * point value, according to the string conversion of each element.
  514. *
  515. * @method sort
  516. * @param [sortFunction=null] {Function}
  517. * @public
  518. * @return {Array.<any>} Returns the list of models in the collection.
  519. * @example
  520. * let sortByDate = function(a, b){
  521. * return new Date(a.date) - new Date(b.date)
  522. * }
  523. *
  524. * collection.sort(sortByDate);
  525. */
  526. public sort(sortFunction = null):Array<any>
  527. {
  528. this.models.sort(sortFunction);
  529.  
  530. return this.models;
  531. }
  532.  
  533. /**
  534. * The filter method creates a new array with all elements that pass the test implemented by the provided function.
  535. *
  536. * @method filter
  537. * @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.
  538. * @param [callbackScope=null] Optional. Value to use as this when executing callback.
  539. * @public
  540. * @return {Array.<any>} Returns the list of models in the collection.
  541. * @example
  542. * let isOldEnough = function(model){
  543. * return model.age >= 21;
  544. * }
  545. *
  546. * let list = collection.filter(isOldEnough);
  547. */
  548. public filter(callback:any, callbackScope:any = null):Array<any>
  549. {
  550. return this.models.filter(callback, callbackScope);
  551. }
  552.  
  553. /**
  554. * Convenient way to get a list of property values.
  555. *
  556. * @method pluck
  557. * @param propertyName {string} The property name you want the values from.
  558. * @param [unique=false] {string} Pass in true to remove duplicates.
  559. * @return {Array.<any>}
  560. * @public
  561. * @example
  562. * collection.add([{name: 'Robert'}, {name: 'Robert'}, {name: 'Chris'}]);
  563. *
  564. * let list = collection.pluck('name');
  565. * // ['Robert', 'Robert', 'Chris']
  566. *
  567. * let list = collection.pluck('name', true);
  568. * // ['Robert', 'Chris']
  569. */
  570. public pluck(propertyName:string, unique:boolean = false):Array<any>
  571. {
  572. let list:Array<any> = [];
  573.  
  574. for (let i = 0; i < this.length; i++) {
  575. if (this.models[i].hasOwnProperty(propertyName) === true) {
  576. list[i] = this.models[i][propertyName];
  577. }
  578. }
  579.  
  580. if (unique === true) {
  581. list = Util.unique(list);
  582. }
  583.  
  584. return list;
  585. }
  586.  
  587. /**
  588. * Convenient way to group models into categories/groups by a property name.
  589. *
  590. * @method groupBy
  591. * @param propertyName {string} The string value of the property you want to group with.
  592. * @return {any} Returns an object that is categorized by the property name.
  593. * @public
  594. * @example
  595. * collection.add([{name: 'Robert', id: 0}, {name: 'Robert', id: 1}, {name: 'Chris', id: 2}]);
  596. *
  597. * let list = collection.groupBy('name');
  598. *
  599. * // {
  600. * // Robert: [{name: 'Robert', id: 0}, {name: 'Robert', id: 1}]
  601. * // Chris: [{name: 'Chris', id: 2}]
  602. * // }
  603. */
  604. public groupBy(propertyName):any
  605. {
  606. let model:any;
  607. let groupName:string;
  608. let groupList:any = {};
  609.  
  610. // Loop through all the models in this collection.
  611. for (let i:number = 0; i < this.length; i++) {
  612. model = this.models[i];
  613. // Get the value from the property name passed in and uses that as the group name.
  614. groupName = model[propertyName];
  615.  
  616. if (groupList[groupName] == null) {
  617. groupList[groupName] = [];
  618. }
  619. groupList[groupName].push(model);
  620. }
  621. return groupList;
  622. }
  623.  
  624. /**
  625. * Changes the order of the models so that the last model becomes the first model, the penultimate model becomes the second, and so on.
  626. *
  627. * @method reverse
  628. * @public
  629. * @return {Array.<any>} Returns the list of models in the collection.
  630. * @example
  631. * collection.reverse();
  632. */
  633. public reverse():Array<any>
  634. {
  635. return this.models.reverse();
  636. }
  637.  
  638. }
  639.  
  640. export default Collection;
  641.