import DataObject from './DataObject.js'
/**
Holds a map of <string, value> pairs, sometimes fetched from or sent to a back-end server.
@example <caption>Directly loading data</caption>
this.model = new DataModel({ id: 42, something: 'different' })
this.model.get('id') // returns 42
this.model.get('something') // returns 'different'
this.model.get('bogus', 'default') // returns 'default'
@example <caption>Fetching data from a service</caption>
* class ExampleModel extends DataModel {
* get url() { return '/api/example' }
* }
* this.model = new ExampleModel()
* this.model.fetch().then(() => { ... }).catch(err => { ... })
*/
const DataModel = class extends DataObject {
/**
@param {Object} [data={}]
@param {Object} [options={}]
@param {Object} [options.fieldDataObjects=null] a map of dataField (string) to DataObject (class), used to create sub-objects in this Model's data
*/
constructor(data = {}, options = {}) {
super(options)
if (typeof this.options.fieldDataObjects === 'undefined') {
this.options.fieldDataObjects = {}
}
/** @type {Object<string,*>} */
this._data = {}
/** @type {DataCollection|null} */
this.collection = null // set or unset by a DataCollection that contains this model
this.setBatch(data)
}
/**
See {@link DataObject.cleanup} for details.
*/
cleanup() {
super.cleanup()
// TODO - need to clean up any sub-DataObjects
this._data = null
}
/**
@param {string} dataField
@return {boolean} true if this model contains a field with the name of `dataField`
*/
has(dataField) {
return typeof this._data[dataField] !== 'undefined'
}
/**
Find a value held within this DataModel.
@param {string} dataField
@param {*} [defaultValue=null] a value to return if the field value is `undefined`, `null`, or the empty string
@return {*} may be native types or, if mapped by options.fieldDataObjects, another DataObject
*/
get(dataField, defaultValue = null) {
if (
typeof this._data[dataField] === 'undefined' ||
this._data[dataField] === null ||
this._data[dataField] === ''
) {
return defaultValue
}
return this._data[dataField]
}
/**
Return the first value that this `DataModel.has` or 'undefined' if none are found
@param {string[]} dataFields
@return {*} may be undefined, native types, or (if mapped by options.fieldDataObjects) another DataObject
*/
getFirst(...dataFields) {
for (let i = 0; i < dataFields.length; i += 1) {
if (this.has(dataFields[i])) {
return this.get(dataFields[i])
}
}
return undefined
}
/**
Set a key/value pair
@param {string} dataField
@param {*} value - the new value of the field
*/
set(dataField, value) {
const batch = {}
batch[dataField] = value
return this.setBatch(batch)
}
/**
Set a group of values. The 'values' parameter should be an object that works in for(key in values) loops like a dictionary: {}
If a key is in options.fieldDataObjects then the value will be used to contruct a DataObject and that will be the saved value.
*/
setBatch(values) {
const changes = {}
let changed = false
for (const key in values) {
const result = this._set(key, values[key])
if (result !== DataObject._NO_CHANGE) {
changed = true
changes[key] = result
this.trigger(`changed:${key}`, this, key, result)
}
}
if (changed) {
this.trigger('changed', this, changes)
}
return changes
}
/**
Add a value to a field, creating the value if necessary
@param {string} dataField
@param {int} [amount=1]
@return {*} the new value of the field
*/
increment(dataField, amount = 1) {
const currentVal = dataField in this._data ? this._data[dataField] : 0
this.set(dataField, currentVal + amount)
return this.get(dataField)
}
_set(dataField, data) {
// _set does not fire any events, so you probably want to use set or setBatch
if (data instanceof DataObject) {
if (this._data[dataField] instanceof DataObject) {
this._data[dataField].reset(data._data)
} else {
this._data[dataField] = data
}
} else if (this.options.fieldDataObjects[dataField]) {
if (this._data[dataField]) {
this._data[dataField].reset(data)
} else {
this._data[dataField] = new this.options.fieldDataObjects[dataField](data)
}
} else {
if (this._data[dataField] === data) {
return DataObject._NO_CHANGE
}
if (this._data[dataField] instanceof DataObject) {
this._data[dataField].reset(data)
} else {
this._data[dataField] = data
}
}
return this._data[dataField]
}
/**
@return {string} The model data as a JSON-formatted string
*/
stringify() {
return JSON.stringify(this._data)
}
/**
Calls the DELETE method on the service resource
@return {Promise}
*/
delete() {
return new Promise((resolve, reject) => {
super
.delete()
.then((...params) => {
if (this.collection !== null) {
this.collection.remove(this)
}
resolve(...params)
})
.catch((...params) => {
reject(...params)
})
})
}
/**
@param {Object} [data={}]
*/
reset(data = {}) {
for (const key in this._data) {
if (typeof data[key] === 'undefined') {
this._data[key] = null
}
}
this.setBatch(data)
this.trigger('reset', this)
}
/**
@param {*} obj - the value to compare to `this` or `this.get('id')`
*/
equals(obj) {
if (obj === null || typeof obj === 'undefined') return false
if (this === obj) return true
if (typeof obj !== typeof this) return false
if (obj.get('id') === this.get('id')) return true
return false
}
}
DataModel.SavingEvent = Symbol('do-saving')
DataModel.SavedEvent = Symbol('do-saved')
export default DataModel
export { DataModel }