Home Reference Source

src/DataCollection.js

import DataObject from './DataObject.js'
import DataModel from './DataModel.js'

/**
	DataCollection represents an ordered list of DataModel instances
*/
const DataCollection = class extends DataObject {
	/**
	@param {Object[]} [data=null]
	@param {Object} [options={}]
	@param {class} [options.dataObject] the DataObject extending class to use to wrap each data item in this collection
	*/
	constructor(data = [], options = {}) {
		super(options)
		if (data == null) data = []
		this._inReset = false
		this._inAddBatch = false
		this._boundRelayListener = this._relayListener.bind(this)
		this.dataObjects = []
		for (const datum of data) {
			this.add(this.generateDataObject(datum))
		}
	}
	cleanup() {
		super.cleanup()
		for (const obj of this.dataObjects) {
			obj.removeListener(this._boundRelayListener)
		}
		this.dataObjects.length = 0
	}
	at(index) {
		if (index < 0 || index > this.dataObjects.length - 1) {
			throw new Error(`Index out of range: ${index}`)
		}
		return this.dataObjects[index]
	}
	create(data, options = {}) {
		// Creates an child instance and POSTs it to the collection
		return new Promise(
			function(resolve, reject) {
				const fetchOptions = Object.assign(options, this.fetchOptions)
				fetchOptions.method = 'post'
				fetchOptions.body = JSON.stringify(data)
				this._innerFetch(this.url, fetchOptions)
					.then(response => {
						if (response.status != 200) {
							throw 'Create failed with status ' + response.status
						}
						return response.json()
					})
					.then(data => {
						const dataObject = this.generateDataObject(data)
						this.add(dataObject)
						resolve(dataObject)
					})
					.catch(reject)
			}.bind(this)
		)
	}
	add(dataObject) {
		if (dataObject instanceof DataObject == false) {
			dataObject = this.generateDataObject(dataObject)
		}
		if (this.dataObjects.indexOf(dataObject) !== -1) {
			// TODO stop using indexOf because equality doesn't work
			return
		}
		this.dataObjects.push(dataObject)
		dataObject.collection = this
		this.trigger('added', this, dataObject)
		if (this._comparator && this._inReset == false && this._inAddBatch == false) {
			this.sort(this._comparator)
		}
		dataObject.addListener(this._boundRelayListener)
	}
	_relayListener(...params) {
		this.trigger(...params)
	}
	// Add an array of DataObjects to the end of the collection
	addBatch(dataObjects) {
		this._inAddBatch = true
		for (let dataObject in dataObjects) {
			if (dataObject instanceof DataObject == false) {
				dataObject = this.generateDataObject(dataObject)
			}
			this.add(dataObject)
		}
		this._inAddBatch = false
	}
	indexOf(dataObject) {
		for (let i = 0; i < this.dataObjects.length; i++) {
			if (this.dataObjects[i].equals(dataObject)) {
				return i
			}
		}
		return -1
	}
	firstByField(dataField, value) {
		for (const model of this) {
			if (model.get(dataField) === value) {
				return model
			}
		}
		return null
	}
	remove(dataObject) {
		const index = this.indexOf(dataObject)
		if (index === -1) {
			return
		}
		this.dataObjects[index].removeListener(this._boundRelayListener)
		this.dataObjects.splice(index, 1)
		dataObject.collection = null
		this.trigger('removed', this, dataObject)
	}
	reset(data) {
		this._inReset = true
		for (const obj of this.dataObjects.slice()) {
			this.remove(obj)
		}
		for (const datum of data) {
			this.add(this.generateDataObject(datum))
		}
		this._inReset = false
		if (this._comparator) {
			this.sort(this._comparator)
		}
		this.trigger('reset', this)
	}
	sort(comparator = DataCollection.defaultComparator) {
		this.dataObjects.sort(comparator)
		this.trigger('sorted', this)
	}
	sortByAttribute(attributeName, comparator = DataCollection.defaultComparator) {
		this.sort((obj1, obj2) => {
			return comparator(obj1.get(attributeName), obj2.get(attributeName))
		})
	}
	keepSortedByField(dataField, comparator = DataCollection.defaultComparator) {
		this._comparator = (obj1, obj2) => {
			return comparator(obj1.get(dataField), obj2.get(dataField))
		}
		this.addListener(() => {
			if (this._comparator && this._inReset == false && this._inAddBatch == false) {
				this.sort(this._comparator)
			}
		}, 'changed:' + dataField)
	}
	*[Symbol.iterator]() {
		for (const obj of this.dataObjects) {
			yield obj
		}
	}
	get length() {
		return this.dataObjects.length
	}
	generateDataObject(data) {
		const options = { collection: this }
		let dataObj
		if (this.options.dataObject) {
			dataObj = new this.options.dataObject(data, options)
		} else {
			dataObj = new DataModel(data, options)
		}
		dataObj._new = false
		return dataObj
	}
}

DataCollection.defaultComparator = function(dataObject1, dataObject2) {
	if (dataObject1 === dataObject2) return 0
	if (typeof dataObject1.equals === 'function' && dataObject1.equals(dataObject2)) return 0
	if (typeof dataObject1.get === 'function' && typeof dataObject2.get === 'function') {
		const val1 = dataObject1.get('id', -1)
		const val2 = dataObject2.get('id', -1)
		if (val1 === val2) return 0
		if (val1 < val2) return -1
		return 1
	}
	if (dataObject1 < dataObject2) return -1
	return 1
}

export default DataCollection