1 /** 2 * Emily.js - http://flams.github.com/emily/ 3 * Copyright(c) 2012-2014 Olivier Scherrer <pode.fr@gmail.com> 4 * MIT Licensed 5 */ 6 "use strict"; 7 8 var Observable = require("./Observable"), 9 Tools = require("./Tools"); 10 11 /** 12 * @class 13 * Store creates an observable structure based on a key/values object 14 * or on an array 15 * @param {Array/Object} the data to initialize the store with 16 * @returns 17 */ 18 module.exports = function StoreConstructor($data) { 19 20 /** 21 * Where the data is stored 22 * @private 23 */ 24 var _data = Tools.clone($data) || {}, 25 26 /** 27 * The observable for publishing changes on the store iself 28 * @private 29 */ 30 _storeObservable = new Observable(), 31 32 /** 33 * The observable for publishing changes on a value 34 * @private 35 */ 36 _valueObservable = new Observable(), 37 38 /** 39 * Saves the handles for the subscriptions of the computed properties 40 * @private 41 */ 42 _computed = [], 43 44 /** 45 * Gets the difference between two objects and notifies them 46 * @private 47 * @param {Object} previousData 48 */ 49 _notifyDiffs = function _notifyDiffs(previousData) { 50 var diffs = Tools.objectsDiffs(previousData, _data); 51 ["updated", 52 "deleted", 53 "added"].forEach(function (value) { 54 diffs[value].forEach(function (dataIndex) { 55 _storeObservable.notify(value, dataIndex, _data[dataIndex]); 56 _valueObservable.notify(dataIndex, _data[dataIndex], value); 57 }); 58 }); 59 }; 60 61 /** 62 * Get the number of items in the store 63 * @returns {Number} the number of items in the store 64 */ 65 this.getNbItems = function() { 66 return _data instanceof Array ? _data.length : Tools.count(_data); 67 }; 68 69 /** 70 * Count is an alias for getNbItems 71 * @returns {Number} the number of items in the store 72 */ 73 this.count = this.getNbItems; 74 75 /** 76 * Get a value from its index 77 * @param {String} name the name of the index 78 * @returns the value 79 */ 80 this.get = function get(name) { 81 return _data[name]; 82 }; 83 84 /** 85 * Checks if the store has a given value 86 * @param {String} name the name of the index 87 * @returns {Boolean} true if the value exists 88 */ 89 this.has = function has(name) { 90 return _data.hasOwnProperty(name); 91 }; 92 93 /** 94 * Set a new value and overrides an existing one 95 * @param {String} name the name of the index 96 * @param value the value to assign 97 * @returns true if value is set 98 */ 99 this.set = function set(name, value) { 100 var hasPrevious, 101 previousValue, 102 action; 103 104 if (typeof name != "undefined") { 105 hasPrevious = this.has(name); 106 previousValue = this.get(name); 107 _data[name] = value; 108 action = hasPrevious ? "updated" : "added"; 109 _storeObservable.notify(action, name, _data[name], previousValue); 110 _valueObservable.notify(name, _data[name], action, previousValue); 111 return true; 112 } else { 113 return false; 114 } 115 }; 116 117 /** 118 * Update the property of an item. 119 * @param {String} name the name of the index 120 * @param {String} property the property to modify. 121 * @param value the value to assign 122 * @returns false if the Store has no name index 123 */ 124 this.update = function update(name, property, value) { 125 var item; 126 if (this.has(name)) { 127 item = this.get(name); 128 Tools.setNestedProperty(item, property, value); 129 _storeObservable.notify("updated", property, value); 130 _valueObservable.notify(name, item, "updated"); 131 return true; 132 } else { 133 return false; 134 } 135 }; 136 137 /** 138 * Delete value from its index 139 * @param {String} name the name of the index from which to delete the value 140 * @returns true if successfully deleted. 141 */ 142 this.del = function del(name) { 143 if (this.has(name)) { 144 if (!this.alter("splice", name, 1)) { 145 delete _data[name]; 146 _storeObservable.notify("deleted", name); 147 _valueObservable.notify(name, _data[name], "deleted"); 148 } 149 return true; 150 } else { 151 return false; 152 } 153 }; 154 155 /** 156 * Delete multiple indexes. Prefer this one over multiple del calls. 157 * @param {Array} 158 * @returns false if param is not an array. 159 */ 160 this.delAll = function delAll(indexes) { 161 if (indexes instanceof Array) { 162 // Indexes must be removed from the greatest to the lowest 163 // To avoid trying to remove indexes that don't exist. 164 // i.e: given [0, 1, 2], remove 1, then 2, 2 doesn't exist anymore 165 indexes.sort(Tools.compareNumbers) 166 .reverse() 167 .forEach(this.del, this); 168 return true; 169 } else { 170 return false; 171 } 172 }; 173 174 /** 175 * Alter the data by calling one of it's method 176 * When the modifications are done, it notifies on changes. 177 * If the function called doesn't alter the data, consider using proxy instead 178 * which is much, much faster. 179 * @param {String} func the name of the method 180 * @params {*} any number of params to be given to the func 181 * @returns the result of the method call 182 */ 183 this.alter = function alter(func) { 184 var apply, 185 previousData; 186 187 if (_data[func]) { 188 previousData = Tools.clone(_data); 189 apply = this.proxy.apply(this, arguments); 190 _notifyDiffs(previousData); 191 _storeObservable.notify("altered", _data, previousData); 192 return apply; 193 } else { 194 return false; 195 } 196 }; 197 198 /** 199 * Proxy is similar to alter but doesn't trigger events. 200 * It's preferable to call proxy for functions that don't 201 * update the interal data source, like slice or filter. 202 * @param {String} func the name of the method 203 * @params {*} any number of params to be given to the func 204 * @returns the result of the method call 205 */ 206 this.proxy = function proxy(func) { 207 if (_data[func]) { 208 return _data[func].apply(_data, Array.prototype.slice.call(arguments, 1)); 209 } else { 210 return false; 211 } 212 }; 213 214 /** 215 * Watch the store's modifications 216 * @param {String} added/updated/deleted 217 * @param {Function} func the function to execute 218 * @param {Object} scope the scope in which to execute the function 219 * @returns {Handle} the subscribe's handler to use to stop watching 220 */ 221 this.watch = function watch(name, func, scope) { 222 return _storeObservable.watch(name, func, scope); 223 }; 224 225 /** 226 * Unwatch the store modifications 227 * @param {Handle} handle the handler returned by the watch function 228 * @returns 229 */ 230 this.unwatch = function unwatch(handle) { 231 return _storeObservable.unwatch(handle); 232 }; 233 234 /** 235 * Get the observable used for watching store's modifications 236 * Should be used only for debugging 237 * @returns {Observable} the Observable 238 */ 239 this.getStoreObservable = function getStoreObservable() { 240 return _storeObservable; 241 }; 242 243 /** 244 * Watch a value's modifications 245 * @param {String} name the name of the value to watch for 246 * @param {Function} func the function to execute 247 * @param {Object} scope the scope in which to execute the function 248 * @returns handler to pass to unwatchValue 249 */ 250 this.watchValue = function watchValue(name, func, scope) { 251 return _valueObservable.watch(name, func, scope); 252 }; 253 254 /** 255 * Unwatch the value's modifications 256 * @param {Handler} handler the handler returned by the watchValue function 257 * @private 258 * @returns true if unwatched 259 */ 260 this.unwatchValue = function unwatchValue(handler) { 261 return _valueObservable.unwatch(handler); 262 }; 263 264 /** 265 * Get the observable used for watching value's modifications 266 * Should be used only for debugging 267 * @private 268 * @returns {Observable} the Observable 269 */ 270 this.getValueObservable = function getValueObservable() { 271 return _valueObservable; 272 }; 273 274 /** 275 * Loop through the data 276 * @param {Function} func the function to execute on each data 277 * @param {Object} scope the scope in wich to run the callback 278 */ 279 this.loop = function loop(func, scope) { 280 Tools.loop(_data, func, scope); 281 }; 282 283 /** 284 * Reset all data and get notifications on changes 285 * @param {Arra/Object} data the new data 286 * @returns {Boolean} 287 */ 288 this.reset = function reset(data) { 289 if (data instanceof Object) { 290 var previousData = Tools.clone(_data); 291 _data = Tools.clone(data) || {}; 292 _notifyDiffs(previousData); 293 _storeObservable.notify("resetted", _data, previousData); 294 return true; 295 } else { 296 return false; 297 } 298 299 }; 300 301 /** 302 * Compute a new property from other properties. 303 * The computed property will look exactly similar to any none 304 * computed property, it can be watched upon. 305 * @param {String} name the name of the computed property 306 * @param {Array} computeFrom a list of properties to compute from 307 * @param {Function} callback the callback to compute the property 308 * @param {Object} scope the scope in which to execute the callback 309 * @returns {Boolean} false if wrong params given to the function 310 */ 311 this.compute = function compute(name, computeFrom, callback, scope) { 312 var args = []; 313 314 if (typeof name == "string" && 315 typeof computeFrom == "object" && 316 typeof callback == "function" && 317 !this.isCompute(name)) { 318 319 _computed[name] = []; 320 321 Tools.loop(computeFrom, function (property) { 322 _computed[name].push(this.watchValue(property, function () { 323 this.set(name, callback.call(scope)); 324 }, this)); 325 }, this); 326 327 this.set(name, callback.call(scope)); 328 return true; 329 } else { 330 return false; 331 } 332 }; 333 334 /** 335 * Remove a computed property 336 * @param {String} name the name of the computed to remove 337 * @returns {Boolean} true if the property is removed 338 */ 339 this.removeCompute = function removeCompute(name) { 340 if (this.isCompute(name)) { 341 Tools.loop(_computed[name], function (handle) { 342 this.unwatchValue(handle); 343 }, this); 344 this.del(name); 345 return true; 346 } else { 347 return false; 348 } 349 }; 350 351 /** 352 * Tells if a property is a computed property 353 * @param {String} name the name of the property to test 354 * @returns {Boolean} true if it's a computed property 355 */ 356 this.isCompute = function isCompute(name) { 357 return !!_computed[name]; 358 }; 359 360 /** 361 * Returns a JSON version of the data 362 * Use dump if you want all the data as a plain js object 363 * @returns {String} the JSON 364 */ 365 this.toJSON = function toJSON() { 366 return JSON.stringify(_data); 367 }; 368 369 /** 370 * Returns the store's data 371 * @returns {Object} the data 372 */ 373 this.dump = function dump() { 374 return _data; 375 }; 376 };