observable-store
Version:
An observable data store with dirty checking and computed properties
381 lines (343 loc) • 11.7 kB
JavaScript
/**
* @license observable-store https://github.com/flams/observable-store
*
* The MIT License (MIT)
*
* Copyright (c) 2014 Olivier Scherrer <pode.fr@gmail.com>
*/
"use strict";
var Observable = require("watch-notify"),
diff = require("shallow-diff"),
clone = require("shallow-copy"),
compareNumbers = require("compare-numbers"),
count = require("object-count"),
nestedProperty = require("nested-property"),
simpleLoop = require("simple-loop");
/**
* @class
* Store creates an observable structure based on a key/values object
* or on an array
* @param {Array/Object} the data to initialize the store with
* @returns
*/
module.exports = function StoreConstructor($data) {
/**
* Where the data is stored
* @private
*/
var _data = clone($data) || {},
/**
* The observable for publishing changes on the store iself
* @private
*/
_storeObservable = new Observable(),
/**
* The observable for publishing changes on a value
* @private
*/
_valueObservable = new Observable(),
/**
* Saves the handles for the subscriptions of the computed properties
* @private
*/
_computed = [],
/**
* Gets the difference between two objects and notifies them
* @private
* @param {Object} previousData
*/
_notifyDiffs = function _notifyDiffs(previousData) {
var diffs = diff(previousData, _data);
["updated",
"deleted",
"added"].forEach(function (value) {
diffs[value].forEach(function (dataIndex) {
_storeObservable.notify(value, dataIndex, _data[dataIndex], previousData[dataIndex]);
_valueObservable.notify(dataIndex, _data[dataIndex], value, previousData[dataIndex]);
});
});
};
/**
* Get the number of items in the store
* @returns {Number} the number of items in the store
*/
this.count = function() {
return count(_data);
};
/**
* Get a value from its index
* @param {String} name the name of the index
* @returns the value
*/
this.get = function get(name) {
return _data[name];
};
/**
* Checks if the store has a given value
* @param {String} name the name of the index
* @returns {Boolean} true if the value exists
*/
this.has = function has(name) {
return _data.hasOwnProperty(name);
};
/**
* Set a new value and overrides an existing one
* @param {String} name the name of the index
* @param value the value to assign
* @returns true if value is set
*/
this.set = function set(name, value) {
var hasPrevious,
previousValue,
action;
if (typeof name != "undefined") {
hasPrevious = this.has(name);
previousValue = this.get(name);
_data[name] = value;
action = hasPrevious ? "updated" : "added";
_storeObservable.notify(action, name, _data[name], previousValue);
_valueObservable.notify(name, _data[name], action, previousValue);
return true;
} else {
return false;
}
};
/**
* Update the property of an item.
* @param {String} name the name of the index
* @param {String} property the property to modify.
* @param value the value to assign
* @returns false if the Store has no name index
*/
this.update = function update(name, property, value) {
var item;
if (this.has(name)) {
item = this.get(name);
nestedProperty.set(item, property, value);
_storeObservable.notify("updated", property, value);
_valueObservable.notify(name, item, "updated");
return true;
} else {
return false;
}
};
/**
* Delete value from its index
* @param {String} name the name of the index from which to delete the value
* @returns true if successfully deleted.
*/
this.del = function del(name) {
var previous;
if (this.has(name)) {
if (!this.alter("splice", name, 1)) {
previous = _data[name];
delete _data[name];
_storeObservable.notify("deleted", name, undefined, previous);
_valueObservable.notify(name, _data[name], "deleted", previous);
}
return true;
} else {
return false;
}
};
/**
* Delete multiple indexes. Prefer this one over multiple del calls.
* @param {Array}
* @returns false if param is not an array.
*/
this.delAll = function delAll(indexes) {
if (Array.isArray(indexes)) {
// Indexes must be removed from the greatest to the lowest
// To avoid trying to remove indexes that don't exist.
// i.e: given [0, 1, 2], remove 1, then 2, 2 doesn't exist anymore
indexes.sort(compareNumbers.desc)
.forEach(this.del, this);
return true;
} else {
return false;
}
};
/**
* Alter the data by calling one of it's method
* When the modifications are done, it notifies on changes.
* If the function called doesn't alter the data, consider using proxy instead
* which is much, much faster.
* @param {String} func the name of the method
* @params {*} any number of params to be given to the func
* @returns the result of the method call
*/
this.alter = function alter(func) {
var apply,
previousData;
if (_data[func]) {
previousData = clone(_data);
apply = this.proxy.apply(this, arguments);
_notifyDiffs(previousData);
_storeObservable.notify("altered", _data, previousData);
return apply;
} else {
return false;
}
};
/**
* Proxy is similar to alter but doesn't trigger events.
* It's preferable to call proxy for functions that don't
* update the interal data source, like slice or filter.
* @param {String} func the name of the method
* @params {*} any number of params to be given to the func
* @returns the result of the method call
*/
this.proxy = function proxy(func) {
if (_data[func]) {
return _data[func].apply(_data, Array.prototype.slice.call(arguments, 1));
} else {
return false;
}
};
/**
* Watch the store's modifications
* @param {String} added/updated/deleted
* @param {Function} func the function to execute
* @param {Object} scope the scope in which to execute the function
* @returns {Handle} the subscribe's handler to use to stop watching
*/
this.watch = function watch(name, func, scope) {
return _storeObservable.watch(name, func, scope);
};
/**
* Unwatch the store modifications
* @param {Handle} handle the handler returned by the watch function
* @returns
*/
this.unwatch = function unwatch(handle) {
return _storeObservable.unwatch(handle);
};
/**
* Get the observable used for watching store's modifications
* Should be used only for debugging
* @returns {Observable} the Observable
*/
this.getStoreObservable = function getStoreObservable() {
return _storeObservable;
};
/**
* Watch a value's modifications
* @param {String} name the name of the value to watch for
* @param {Function} func the function to execute
* @param {Object} scope the scope in which to execute the function
* @returns handler to pass to unwatchValue
*/
this.watchValue = function watchValue(name, func, scope) {
return _valueObservable.watch(name, func, scope);
};
/**
* Unwatch the value's modifications
* @param {Handler} handler the handler returned by the watchValue function
* @private
* @returns true if unwatched
*/
this.unwatchValue = function unwatchValue(handler) {
return _valueObservable.unwatch(handler);
};
/**
* Get the observable used for watching value's modifications
* Should be used only for debugging
* @private
* @returns {Observable} the Observable
*/
this.getValueObservable = function getValueObservable() {
return _valueObservable;
};
/**
* Loop through the data
* @param {Function} func the function to execute on each data
* @param {Object} scope the scope in wich to run the callback
*/
this.loop = function loop(func, scope) {
simpleLoop(_data, func, scope);
};
/**
* Reset all data and get notifications on changes
* @param {Arra/Object} data the new data
* @returns {Boolean}
*/
this.reset = function reset(data) {
if (typeof data == "object") {
var previousData = clone(_data);
_data = clone(data) || {};
_notifyDiffs(previousData);
_storeObservable.notify("resetted", _data, previousData);
return true;
} else {
return false;
}
};
/**
* Compute a new property from other properties.
* The computed property will look exactly similar to any none
* computed property, it can be watched upon.
* @param {String} name the name of the computed property
* @param {Array} computeFrom a list of properties to compute from
* @param {Function} callback the callback to compute the property
* @param {Object} scope the scope in which to execute the callback
* @returns {Boolean} false if wrong params given to the function
*/
this.compute = function compute(name, computeFrom, callback, scope) {
var args = [];
if (typeof name == "string" &&
typeof computeFrom == "object" &&
typeof callback == "function" &&
!this.isCompute(name)) {
_computed[name] = [];
simpleLoop(computeFrom, function (property) {
_computed[name].push(this.watchValue(property, function () {
this.set(name, callback.call(scope));
}, this));
}, this);
this.set(name, callback.call(scope));
return true;
} else {
return false;
}
};
/**
* Remove a computed property
* @param {String} name the name of the computed to remove
* @returns {Boolean} true if the property is removed
*/
this.removeCompute = function removeCompute(name) {
if (this.isCompute(name)) {
simpleLoop(_computed[name], function (handle) {
this.unwatchValue(handle);
}, this);
this.del(name);
delete _computed[name];
return true;
} else {
return false;
}
};
/**
* Tells if a property is a computed property
* @param {String} name the name of the property to test
* @returns {Boolean} true if it's a computed property
*/
this.isCompute = function isCompute(name) {
return !!_computed[name];
};
/**
* Returns a JSON version of the data
* Use dump if you want all the data as a plain js object
* @returns {String} the JSON
*/
this.toJSON = function toJSON() {
return JSON.stringify(_data);
};
/**
* Returns the store's data
* @returns {Object} the data
*/
this.dump = function dump() {
return _data;
};
};