Comparing version 2.0.0-dev1 to 2.0.0-dev10
@@ -7,5 +7,7 @@ # Changelog | ||
* Cursor's setters method won't return themselves but rather the affected node now. | ||
* Adding `cursor.append` and `cursor.prepend`, replacing some `cursor.push` and `cursor.unshift` weird behaviors. | ||
* Adding the `cursor.serialize` method. | ||
* Adding the `cursor.project` method. | ||
* Adding `cursor.concat`. | ||
* Adding `cursor.serialize`. | ||
* Adding `cursor.project`. | ||
* Adding `cursor.exists`. | ||
* Adding `cursor.watch`. | ||
* Changing the way you can define computed data in the tree, aka "facets". Facets are now to be defined within the tree itself and can be accessed using the exact same API as normal data. | ||
@@ -16,5 +18,6 @@ * Adding an alternative facet definition syntax for convenience. | ||
* Update events are now exposing the detail of each transaction so you can replay them elsewhere. | ||
* Fixing `cursor.push/unshift` behavior. | ||
* Dropped the `$cursor` helper. | ||
* Dropped the `update` specs for a simpler transaction syntax. | ||
* Updated `emmett` to `3.1.0`. | ||
* Updated `emmett` to `3.1.1`. | ||
* ES6 codebase rewrite. | ||
@@ -21,0 +24,0 @@ * Full code self documentation. |
@@ -63,2 +63,5 @@ /** | ||
// Should the tree be persistent? | ||
persistent: true, | ||
// Validation specifications | ||
@@ -99,2 +102,4 @@ validate: null, | ||
var Baobab = (function (_Emitter) { | ||
_inherits(Baobab, _Emitter); | ||
function Baobab(initialData, opts) { | ||
@@ -116,2 +121,5 @@ var _this = this; | ||
// Disabling immutability if persistence if disabled | ||
if (!this.options.persistent) this.options.immutable = false; | ||
// Privates | ||
@@ -142,3 +150,3 @@ this._identity = '[object Baobab]'; | ||
['append', 'apply', 'get', 'prepend', 'push', 'merge', 'project', 'serialize', 'set', 'splice', 'unset', 'unshift'].forEach(bootstrap); | ||
['apply', 'concat', 'exists', 'get', 'push', 'merge', 'project', 'serialize', 'set', 'splice', 'unset', 'unshift'].forEach(bootstrap); | ||
@@ -149,4 +157,2 @@ // Creating the computed data index for the first time | ||
_inherits(Baobab, _Emitter); | ||
_createClass(Baobab, [{ | ||
@@ -159,8 +165,6 @@ key: '_refreshComputedDataIndex', | ||
* | ||
* @param {array} [path] - Path to the modified node. | ||
* @param {string} [operation] - Type of the applied operation. | ||
* @param {mixed} [node] - The new node. | ||
* @return {Baobab} - The tree itself for chaining purposes. | ||
* @param {array} [path] - Path to the modified node. | ||
* @return {Baobab} - The tree itself for chaining purposes. | ||
*/ | ||
value: function _refreshComputedDataIndex(path, operation, node) { | ||
value: function _refreshComputedDataIndex(path) { | ||
var _this2 = this; | ||
@@ -170,3 +174,3 @@ | ||
var walk = function walk(data) { | ||
var p = arguments[1] === undefined ? [] : arguments[1]; | ||
var p = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1]; | ||
@@ -195,6 +199,13 @@ // Have we reached the end? | ||
if (!path) { | ||
if (!path || !path.length) { | ||
// Walk the whole tree | ||
return walk(this.data); | ||
} else { | ||
// Retrieving parent of affected node | ||
var parentNode = (0, _helpers.getIn)(this.data, path.slice(0, -1)).data; | ||
// Walk the affected leaf | ||
return walk(parentNode, path.slice(0, -1)); | ||
} | ||
@@ -240,3 +251,3 @@ } | ||
if (!cursor) { | ||
cursor = new _cursor2['default'](this, path, hash); | ||
cursor = new _cursor2['default'](this, path, { hash: hash }); | ||
this._cursors[hash] = cursor; | ||
@@ -270,8 +281,9 @@ } | ||
// Stashing previous data if this is the frame's first update | ||
if (!this._transaction.length) this.previousData = this.data; | ||
// Solving the given path | ||
// Applying the operation | ||
var solvedPath = (0, _helpers.solvePath)(this.data, path); | ||
var _getIn = (0, _helpers.getIn)(this.data, path, this._computedDataIndex); | ||
var solvedPath = _getIn.solvedPath; | ||
var exists = _getIn.exists; | ||
// If we couldn't solve the path, we throw | ||
@@ -282,4 +294,12 @@ if (!solvedPath) throw (0, _helpers.makeError)('Baobab.update: could not solve the given path.', { | ||
// We don't unset irrelevant paths | ||
if (operation.type === 'unset' && !exists) return; | ||
// Stashing previous data if this is the frame's first update | ||
if (!this._transaction.length) this.previousData = this.data; | ||
var hash = hashPath(solvedPath); | ||
// Applying the operation | ||
var _update2 = (0, _update4['default'])(this.data, solvedPath, operation, this.options); | ||
@@ -295,2 +315,6 @@ | ||
// Refreshing facet index | ||
// TODO: provide a setting to disable this or at least selectively for perf | ||
this._refreshComputedDataIndex(solvedPath); | ||
// Emitting a `write` event | ||
@@ -300,5 +324,3 @@ this.emit('write', { path: solvedPath }); | ||
// Should we let the user commit? | ||
if (!this.options.autoCommit) { | ||
return node; | ||
} | ||
if (!this.options.autoCommit) return node; | ||
@@ -376,2 +398,17 @@ // Should we update asynchronously? | ||
}, { | ||
key: 'watch', | ||
/** | ||
* Method used to watch a collection of paths within the tree. Very useful | ||
* to bind UI components and such to the tree. | ||
* | ||
* @param {object|array} paths - Paths to listen. | ||
* @return {Cursor} - A special cursor that can be listened. | ||
*/ | ||
value: function watch(paths) { | ||
if (!_type2['default'].object(paths) && !_type2['default'].array(paths)) throw Error('Baobab.watch: wrong argument.'); | ||
return new _cursor2['default'](this, null, { watched: paths }); | ||
} | ||
}, { | ||
key: 'release', | ||
@@ -429,3 +466,3 @@ | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '2.0.0-dev1' | ||
value: '2.0.0-dev10' | ||
}); | ||
@@ -432,0 +469,0 @@ |
@@ -15,3 +15,3 @@ /** | ||
var _get = function get(_x3, _x4, _x5) { var _again = true; _function: while (_again) { var object = _x3, property = _x4, receiver = _x5; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x3 = parent; _x4 = property; _x5 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; | ||
var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { var object = _x4, property = _x5, receiver = _x6; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; | ||
@@ -35,14 +35,35 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } | ||
/** | ||
* Traversal helper function for dynamic cursors. Will throw a legible error | ||
* if traversal is not possible. | ||
* | ||
* @param {string} method - The method name, to create a correct error msg. | ||
* @param {array} solvedPath - The cursor's solved path. | ||
*/ | ||
function checkPossibilityOfDynamicTraversal(method, solvedPath) { | ||
if (!solvedPath) throw (0, _helpers.makeError)('Baobab.Cursor.' + method + ': ' + ('cannot use ' + method + ' on an unresolved dynamic path.'), { path: solvedPath }); | ||
} | ||
/** | ||
* Cursor class | ||
* | ||
* Note: opts.watched is not called opts.watch not to tamper with experimental | ||
* `Object.prototype.watch`. | ||
* | ||
* @constructor | ||
* @param {Baobab} tree - The cursor's root. | ||
* @param {array} path - The cursor's path in the tree. | ||
* @param {string} hash - The path's hash computed ahead by the tree. | ||
* @param {Baobab} tree - The cursor's root. | ||
* @param {array} path - The cursor's path in the tree. | ||
* @param {object} [opts] - Options | ||
* @param {string} [opts.hash] - The path's hash computed ahead by the tree. | ||
* @param {array} [opts.watched] - Parts of the tree the cursor is meant to | ||
* watch over. | ||
*/ | ||
var Cursor = (function (_Emitter) { | ||
function Cursor(tree, path, hash) { | ||
_inherits(Cursor, _Emitter); | ||
function Cursor(tree, path) { | ||
var _this = this; | ||
var opts = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; | ||
_classCallCheck(this, Cursor); | ||
@@ -62,6 +83,7 @@ | ||
this.path = path; | ||
this.hash = hash; | ||
this.hash = opts.hash; | ||
// State | ||
this.state = { | ||
killed: false, | ||
recording: false, | ||
@@ -71,2 +93,18 @@ undoing: false | ||
// Checking whether the cursor is a watcher | ||
if (opts.watched) { | ||
this._watched = (0, _helpers.shallowClone)(opts.watched); | ||
// Normalizing path | ||
for (var k in this._watched) { | ||
if (this._watched[k] instanceof Cursor) this._watched[k] = this._watched[k].path; | ||
} // Keeping track of watched paths | ||
this._watchedPaths = opts.watched && (!_type2['default'].array(this._watched) ? Object.keys(this._watched).map(function (k) { | ||
return _this._watched[k]; | ||
}) : this._watched); | ||
// Overriding the cursor's get method | ||
this.get = this.tree.project.bind(this.tree, this._watched); | ||
} | ||
// Checking whether the given path is dynamic or not | ||
@@ -78,5 +116,19 @@ this._dynamicPath = _type2['default'].dynamicPath(this.path); | ||
if (!this._dynamicPath) this.solvedPath = this.path;else this.solvedPath = (0, _helpers.solvePath)(this.tree.data, this.path); | ||
if (!this._dynamicPath) this.solvedPath = this.path;else this.solvedPath = this._getIn(this.path).solvedPath; | ||
/** | ||
* Listener bound to the tree's writes so that cursors with dynamic paths | ||
* may update their solved path correctly. | ||
* | ||
* @param {object} event - The event fired by the tree. | ||
*/ | ||
this._writeHandler = function (_ref) { | ||
var data = _ref.data; | ||
if (_this.state.killed || !(0, _helpers.solveUpdate)([data.path], _this._getComparedPaths())) return; | ||
_this.solvedPath = _this._getIn(_this.path).solvedPath; | ||
}; | ||
/** | ||
* Function in charge of actually trigger the cursor's updates and | ||
@@ -88,4 +140,7 @@ * deal with the archived records. | ||
var fireUpdate = function fireUpdate(previousData) { | ||
var record = (0, _helpers.getIn)(previousData, _this.solvedPath); | ||
if (_this._watched) return _this.emit('update'); | ||
var record = (0, _helpers.getIn)(previousData, _this.solvedPath).data; | ||
if (_this.state.recording && !_this.state.undoing) _this.archive.add(record); | ||
@@ -112,2 +167,4 @@ | ||
this._updateHandler = function (event) { | ||
if (_this.state.killed) return; | ||
var _event$data = event.data; | ||
@@ -117,18 +174,5 @@ var paths = _event$data.paths; | ||
var update = fireUpdate.bind(_this, previousData); | ||
var comparedPaths = _this._getComparedPaths(); | ||
// Checking whether we should keep track of some dependencies | ||
// TODO: some operations here might be merged for perfs | ||
var additionalPaths = _this._facetPath ? (0, _helpers.getIn)(_this.tree._computedDataIndex, _this._facetPath).paths : []; | ||
// If this is the root selector, we fire already | ||
if (_this.isRoot()) return update(); | ||
// If the cursor's path is dynamic, we need to recompute it | ||
if (_this._dynamicPath) _this.solvedPath = (0, _helpers.solvePath)(_this.tree.data, _this.path); | ||
var shouldFire = false; | ||
if (_this.solvedPath) shouldFire = (0, _helpers.solveUpdate)(paths, [_this.solvedPath].concat(additionalPaths)); | ||
if (shouldFire) return update(); | ||
if ((0, _helpers.solveUpdate)(paths, comparedPaths)) return update(); | ||
}; | ||
@@ -142,2 +186,5 @@ | ||
bound = true; | ||
if (_this._dynamicPath) _this.tree.on('write', _this._writeHandler); | ||
return _this.tree.on('update', _this._updateHandler); | ||
@@ -147,3 +194,2 @@ }; | ||
// If the path is dynamic, we actually need to listen to the tree | ||
// TODO: there should be another way | ||
if (this._dynamicPath) { | ||
@@ -159,5 +205,61 @@ this._lazyBind(); | ||
_inherits(Cursor, _Emitter); | ||
_createClass(Cursor, [{ | ||
key: '_getIn', | ||
_createClass(Cursor, [{ | ||
/** | ||
* Internal helpers | ||
* ----------------- | ||
*/ | ||
/** | ||
* Curried version of the `getIn` helper and ready to serve a cursor instance | ||
* purpose without having to write endlessly the same args over and over. | ||
* | ||
* @param {array} path - The path to get in the tree. | ||
* @return {object} - The result of the `getIn` helper. | ||
*/ | ||
value: function _getIn(path) { | ||
return (0, _helpers.getIn)(this.tree.data, path, this.tree._computedDataIndex, this.tree.options); | ||
} | ||
}, { | ||
key: '_getComparedPaths', | ||
/** | ||
* Method returning the paths of the tree watched over by the cursor and that | ||
* should be taken into account when solving a potential update. | ||
* | ||
* @return {array} - Array of paths to compare with a given update. | ||
*/ | ||
value: function _getComparedPaths() { | ||
var _this2 = this; | ||
var comparedPaths = undefined; | ||
// Standard cursor | ||
if (!this._watched) { | ||
// Checking whether we should keep track of some dependencies | ||
var additionalPaths = this._facetPath ? (0, _helpers.getIn)(this.tree._computedDataIndex, this._facetPath).data.relatedPaths() : []; | ||
comparedPaths = [this.solvedPath].concat(additionalPaths); | ||
} | ||
// Watcher cursor | ||
else { | ||
comparedPaths = this._watchedPaths.reduce(function (cp, p) { | ||
if (_type2['default'].dynamicPath(p)) p = _this2._getIn(p).solvedPath; | ||
if (!p) return cp; | ||
var facetPath = _type2['default'].facetPath(p); | ||
if (facetPath) return cp.concat((0, _helpers.getIn)(_this2.tree._computedDataIndex, p).data.relatedPaths()); | ||
return cp.concat([p]); | ||
}, []); | ||
} | ||
return comparedPaths; | ||
} | ||
}, { | ||
key: 'isRoot', | ||
@@ -245,3 +347,3 @@ | ||
value: function up() { | ||
if (this.solvedPath && !this.isRoot()) return this.tree.select(this.path.slice(0, -1));else return null; | ||
if (!this.isRoot()) return this.tree.select(this.path.slice(0, -1));else return null; | ||
} | ||
@@ -257,2 +359,4 @@ }, { | ||
value: function down() { | ||
checkPossibilityOfDynamicTraversal('down', this.solvedPath); | ||
if (!(this._get().data instanceof Array)) throw Error('Baobab.Cursor.down: cannot go down on a non-list type.'); | ||
@@ -272,2 +376,4 @@ | ||
value: function left() { | ||
checkPossibilityOfDynamicTraversal('left', this.solvedPath); | ||
var last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -289,2 +395,4 @@ | ||
value: function right() { | ||
checkPossibilityOfDynamicTraversal('right', this.solvedPath); | ||
var last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -308,2 +416,4 @@ | ||
value: function leftmost() { | ||
checkPossibilityOfDynamicTraversal('leftmost', this.solvedPath); | ||
var last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -325,2 +435,4 @@ | ||
value: function rightmost() { | ||
checkPossibilityOfDynamicTraversal('rightmost', this.solvedPath); | ||
var last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -345,2 +457,4 @@ | ||
value: function map(fn, scope) { | ||
checkPossibilityOfDynamicTraversal('map', this.solvedPath); | ||
var array = this._get().data, | ||
@@ -374,10 +488,33 @@ l = arguments.length; | ||
value: function _get() { | ||
var path = arguments[0] === undefined ? [] : arguments[0]; | ||
var path = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; | ||
var fullPath = this.solvedPath.concat(path), | ||
data = (0, _helpers.getIn)(this.tree.data, fullPath, this.tree._computedDataIndex, this.tree.options); | ||
if (!_type2['default'].path(path)) throw (0, _helpers.makeError)('Baobab.Cursor.getters: invalid path.', { path: path }); | ||
return { data: data, solvedPath: fullPath }; | ||
if (!this.solvedPath) return { data: undefined, solvedPath: null, exists: false }; | ||
return this._getIn(this.solvedPath.concat(path)); | ||
} | ||
}, { | ||
key: 'exists', | ||
/** | ||
* Method used to check whether a certain path exists in the tree starting | ||
* from the current cursor. | ||
* | ||
* Arity (1): | ||
* @param {path} path - Path to check in the tree. | ||
* | ||
* Arity (2): | ||
* @param {..step} path - Path to check in the tree. | ||
* | ||
* @return {boolean} - Does the given path exists? | ||
*/ | ||
value: function exists(path) { | ||
path = path || path === 0 ? path : []; | ||
if (arguments.length > 1) path = (0, _helpers.arrayFrom)(arguments); | ||
return this._get(path).exists; | ||
} | ||
}, { | ||
key: 'get', | ||
@@ -387,7 +524,5 @@ | ||
* Method used to get data from the tree. Will fire a `get` event from the | ||
* tree so that the user may sometimes react upon it to fecth data, for | ||
* tree so that the user may sometimes react upon it to fetch data, for | ||
* instance. | ||
* | ||
* @todo: check path validity | ||
* | ||
* Arity (1): | ||
@@ -397,3 +532,3 @@ * @param {path} path - Path to get in the tree. | ||
* Arity (2): | ||
* @param {..step} path - Path to get in the tree. | ||
* @param {..step} path - Path to get in the tree. | ||
* | ||
@@ -413,3 +548,3 @@ * @return {mixed} - Data at path. | ||
// Emitting the event | ||
this.tree.emit('get', { data: data, path: solvedPath }); | ||
this.tree.emit('get', { data: data, solvedPath: solvedPath, path: this.path.concat(path) }); | ||
@@ -429,3 +564,3 @@ return data; | ||
* Arity (2): | ||
* @param {..step} path - Path to serialize in the tree. | ||
* @param {..step} path - Path to serialize in the tree. | ||
* | ||
@@ -519,3 +654,3 @@ * @return {mixed} - The retrieved raw data. | ||
value: function undo() { | ||
var steps = arguments[0] === undefined ? 1 : arguments[0]; | ||
var steps = arguments.length <= 0 || arguments[0] === undefined ? 1 : arguments[0]; | ||
@@ -580,7 +715,9 @@ if (!this.state.recording) throw new Error('Baobab.Cursor.undo: cursor is not recording.'); | ||
// Removing listener on parent | ||
// Removing listeners on parent | ||
if (this._dynamicPath) this.tree.off('write', this._writeHandler); | ||
this.tree.off('update', this._updateHandler); | ||
// Unsubscribe from the parent | ||
delete this.tree._cursors[this.hash]; | ||
if (this.hash) delete this.tree._cursors[this.hash]; | ||
@@ -595,2 +732,3 @@ // Dereferencing | ||
this.kill(); | ||
this.state.killed = true; | ||
} | ||
@@ -651,2 +789,8 @@ }, { | ||
* | ||
* Note: this is not really possible to make those setters variadic because | ||
* it would create an impossible polymorphism with path. | ||
* | ||
* @todo: perform value validation elsewhere so that tree.update can | ||
* beneficiate from it. | ||
* | ||
* Arity (1): | ||
@@ -701,7 +845,6 @@ * @param {mixed} value - New value to set at cursor's path. | ||
makeSetter('push'); | ||
makeSetter('append', _type2['default'].array); | ||
makeSetter('concat', _type2['default'].array); | ||
makeSetter('unshift'); | ||
makeSetter('prepend', _type2['default'].array); | ||
makeSetter('splice', _type2['default'].array); | ||
makeSetter('merge', _type2['default'].object); | ||
module.exports = exports['default']; |
@@ -30,3 +30,3 @@ /** | ||
* @param {Baobab} tree - The tree. | ||
* @param {array} path - Path where the facets stands in its tree. | ||
* @param {array} pathInTree - Path where the facets stands in its tree. | ||
* @param {array|object} definition - The facet's definition. | ||
@@ -36,3 +36,3 @@ */ | ||
var Facet = (function () { | ||
function Facet(tree, path, definition) { | ||
function Facet(tree, pathInTree, definition) { | ||
var _this = this; | ||
@@ -46,3 +46,3 @@ | ||
// If the definition type is not valid, we cry | ||
if (!definitionType) throw (0, _helpers.makeError)('Baobab.Facet: attempting to create a computed data node with a ' + ('wrong definition (path: /' + path.join('/') + ').'), { path: path, definition: definition }); | ||
if (!definitionType) throw (0, _helpers.makeError)('Baobab.Facet: attempting to create a computed data node with a ' + ('wrong definition (path: /' + pathInTree.join('/') + ').'), { path: pathInTree, definition: definition }); | ||
@@ -54,2 +54,7 @@ // Properties | ||
// State | ||
this.state = { | ||
killed: false | ||
}; | ||
// Harmonizing | ||
@@ -68,2 +73,11 @@ if (definitionType === 'object') { | ||
this._hasDynamicPaths = this.paths.some(function (p) { | ||
return _type2['default'].dynamicPath(p); | ||
}); | ||
// Is the facet recursive? | ||
this.isRecursive = this.paths.some(function (p) { | ||
return !!_type2['default'].facetPath(p); | ||
}); | ||
// Internal state | ||
@@ -84,4 +98,6 @@ this.state = { | ||
if (_this.state.killed) return; | ||
// Is this facet affected by the current write event? | ||
var concerned = (0, _helpers.solveUpdate)([path], _this.paths); | ||
var concerned = (0, _helpers.solveUpdate)([path], _this.relatedPaths()); | ||
@@ -99,2 +115,29 @@ if (concerned) { | ||
_createClass(Facet, [{ | ||
key: 'relatedPaths', | ||
/** | ||
* Method returning solved paths related to the facet. | ||
* | ||
* @return {array} - An array of related paths. | ||
*/ | ||
value: function relatedPaths() { | ||
var _this2 = this; | ||
var paths = undefined; | ||
if (this._hasDynamicPaths) paths = this.paths.map(function (p) { | ||
return (0, _helpers.getIn)(_this2.tree.data, p, _this2.tree._computedDataIndex).solvedPath; | ||
});else paths = this.paths; | ||
if (!this.isRecursive) return paths;else return paths.reduce(function (accumulatedPaths, path) { | ||
var facetPath = _type2['default'].facetPath(path); | ||
if (!facetPath) return accumulatedPaths.concat(path); | ||
// Solving recursive path | ||
var relatedFacet = (0, _helpers.getIn)(_this2.tree._computedDataIndex, facetPath).data; | ||
return accumulatedPaths.concat(relatedFacet.relatedPaths()); | ||
}, []); | ||
} | ||
}, { | ||
key: 'get', | ||
@@ -139,2 +182,3 @@ | ||
this.tree.off('write', this.listener); | ||
this.state.killed = true; | ||
} | ||
@@ -141,0 +185,0 @@ }]); |
@@ -21,3 +21,2 @@ /** | ||
exports.pathObject = pathObject; | ||
exports.solvePath = solvePath; | ||
exports.solveUpdate = solveUpdate; | ||
@@ -30,2 +29,6 @@ exports.splice = splice; | ||
var _facet = require('./facet'); | ||
var _facet2 = _interopRequireDefault(_facet); | ||
var _type = require('./type'); | ||
@@ -249,19 +252,2 @@ | ||
/** | ||
* Function returning the first element of a list matching the given | ||
* predicate. | ||
* | ||
* @param {array} a - The target array. | ||
* @param {function} fn - The predicate function. | ||
* @return {mixed} - The first matching item or `undefined`. | ||
*/ | ||
function first(a, fn) { | ||
var i = undefined, | ||
l = undefined; | ||
for (i = 0, l = a.length; i < l; i++) { | ||
if (fn(a[i])) return a[i]; | ||
} | ||
return; | ||
} | ||
/** | ||
* Function freezing the given variable if possible. | ||
@@ -315,2 +301,33 @@ * | ||
/** | ||
* Function used to solve a computed data mask by recursively walking a tree | ||
* and patching it. | ||
* | ||
* @param {boolean} immutable - Is the data immutable? | ||
* @param {mixed} data - Data to patch. | ||
* @param {object} mask - Computed data mask. | ||
* @param {object} [parent] - Parent object in the iteration. | ||
* @param {string} [lastKey] - Current value's key in parent. | ||
*/ | ||
function solveMask(immutable, data, mask, parent) { | ||
for (var k in mask) { | ||
if (k[0] === '$') { | ||
// Patching | ||
data[k] = mask[k].get(); | ||
if (immutable) deepFreeze(parent); | ||
} else { | ||
if (immutable) { | ||
data[k] = shallowClone(data[k]); | ||
if (parent) freeze(parent); | ||
} | ||
solveMask(immutable, data[k], mask[k], data); | ||
} | ||
} | ||
return data; | ||
} | ||
/** | ||
* Function retrieving nested data within the given object and according to | ||
@@ -325,13 +342,18 @@ * the given path. | ||
* @param {object} [mask] - An optional computed data index. | ||
* @return {mixed} - The data at path, or if not found, `undefined`. | ||
* @return {object} result - The result. | ||
* @return {mixed} result.data - The data at path, or `undefined`. | ||
* @return {array} result.solvedPath - The solved path or `null`. | ||
*/ | ||
var notFoundObject = { data: undefined, solvedPath: null, exists: false }; | ||
function getIn(object, path) { | ||
var mask = arguments[2] === undefined ? null : arguments[2]; | ||
var opts = arguments[3] === undefined ? {} : arguments[3]; | ||
var mask = arguments.length <= 2 || arguments[2] === undefined ? null : arguments[2]; | ||
var opts = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3]; | ||
path = path || []; | ||
if (!path) return notFoundObject; | ||
var c = object, | ||
var solvedPath = [], | ||
c = object, | ||
cm = mask, | ||
idx = undefined, | ||
i = undefined, | ||
@@ -341,15 +363,24 @@ l = undefined; | ||
for (i = 0, l = path.length; i < l; i++) { | ||
if (!c) return; | ||
if (!c) return { data: undefined, solvedPath: path, exists: false }; | ||
if (typeof path[i] === 'function') { | ||
if (!_type2['default'].array(c)) return; | ||
if (!_type2['default'].array(c)) return notFoundObject; | ||
c = first(c, path[i]); | ||
idx = index(c, path[i]); | ||
if (! ~idx) return notFoundObject; | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} else if (typeof path[i] === 'object') { | ||
if (!_type2['default'].array(c)) return; | ||
if (!_type2['default'].array(c)) return notFoundObject; | ||
c = first(c, function (e) { | ||
idx = index(c, function (e) { | ||
return compare(e, path[i]); | ||
}); | ||
if (! ~idx) return notFoundObject; | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} else { | ||
solvedPath.push(path[i]); | ||
@@ -369,28 +400,9 @@ // Solving data from a facet if needed | ||
// If the mask is still relevant, we continue until we solved computed data | ||
// completely | ||
// If the mask is still relevant, we solve it down to the leaves | ||
if (cm && Object.keys(cm).length) { | ||
(function () { | ||
// TODO: optimize, this is hardly performant | ||
c = deepClone(c); | ||
var walk = function walk(d, m) { | ||
for (var k in m) { | ||
if (k[0] === '$') { | ||
d[k] = m[k].get(); | ||
} else { | ||
walk(d[k], m[k]); | ||
} | ||
} | ||
}; | ||
walk(c, cm); | ||
// Freezing again if immutable | ||
if (opts.immutable) deepFreeze(c); | ||
})(); | ||
var patchedData = solveMask(opts.immutable, { root: c }, { root: cm }); | ||
c = patchedData.root; | ||
} | ||
return c; | ||
return { data: c, solvedPath: solvedPath, exists: c !== undefined }; | ||
} | ||
@@ -437,2 +449,4 @@ | ||
* be used by Baobab's internal and would be unsuited in any other case. | ||
* Note 4): this function will release any facet found on its path to the | ||
* leaves for cleanup reasons. | ||
* | ||
@@ -462,2 +476,6 @@ * @param {boolean} deep - Whether the merge should be deep or not. | ||
} else { | ||
// Releasing | ||
if (o[k] instanceof _facet2['default']) o[k].release(); | ||
o[k] = t[k]; | ||
@@ -522,43 +540,2 @@ } | ||
/** | ||
* Function solving the given path within the target object. | ||
* | ||
* @param {object} object - The object in which the path must be solved. | ||
* @param {array} path - The path to follow. | ||
* @return {mixed} - The solved path if possible, else `null`. | ||
*/ | ||
function solvePath(object, path) { | ||
var solvedPath = [], | ||
c = object, | ||
idx = undefined, | ||
i = undefined, | ||
l = undefined; | ||
for (i = 0, l = path.length; i < l; i++) { | ||
if (!c) return null; | ||
if (typeof path[i] === 'function') { | ||
if (!_type2['default'].array(c)) return; | ||
idx = index(c, path[i]); | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} else if (typeof path[i] === 'object') { | ||
if (!_type2['default'].array(c)) return; | ||
idx = index(c, function (e) { | ||
return compare(e, path[i]); | ||
}); | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} else { | ||
solvedPath.push(path[i]); | ||
c = c[path[i]] || {}; | ||
} | ||
} | ||
return solvedPath; | ||
} | ||
/** | ||
* Function determining whether some paths in the tree were affected by some | ||
@@ -601,3 +578,3 @@ * updates that occurred at the given paths. This helper is mainly used at | ||
if (!c.length) return true; | ||
if (!c || !c.length) return true; | ||
@@ -609,2 +586,3 @@ // Looping through steps | ||
// If path is not relevant, we break | ||
// NOTE: the '!=' instead of '!==' is required here! | ||
if (s != p[k]) break; | ||
@@ -611,0 +589,0 @@ |
@@ -195,3 +195,3 @@ /** | ||
// Ordered by likeliness | ||
var VALID_OPERATIONS = ['set', 'apply', 'push', 'unshift', 'append', 'prepend', 'merge', 'splice', 'unset']; | ||
var VALID_OPERATIONS = ['set', 'apply', 'push', 'unshift', 'concat', 'merge', 'splice', 'unset']; | ||
@@ -198,0 +198,0 @@ type.operationType = function (string) { |
@@ -40,3 +40,3 @@ /** | ||
function update(data, path, operation) { | ||
var opts = arguments[3] === undefined ? {} : arguments[3]; | ||
var opts = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3]; | ||
var operationType = operation.type; | ||
@@ -89,3 +89,3 @@ var value = operation.value; | ||
p[s] = p[s].concat([value]); | ||
if (opts.persistent) p[s] = p[s].concat([value]);else p[s].push(value); | ||
} | ||
@@ -99,24 +99,15 @@ | ||
p[s] = [value].concat(p[s]); | ||
if (opts.persistent) p[s] = [value].concat(p[s]);else p[s].unshift(value); | ||
} | ||
/** | ||
* Append | ||
* Concat | ||
*/ | ||
else if (operationType === 'append') { | ||
if (!_type2['default'].array(p[s])) throw err('append', 'array', currentPath); | ||
else if (operationType === 'concat') { | ||
if (!_type2['default'].array(p[s])) throw err('concat', 'array', currentPath); | ||
p[s] = p[s].concat(value); | ||
if (opts.persistent) p[s] = p[s].concat(value);else p[s].push.apply(p[s], value); | ||
} | ||
/** | ||
* Prepend | ||
*/ | ||
else if (operationType === 'prepend') { | ||
if (!_type2['default'].array(p[s])) throw err('prepend', 'array', currentPath); | ||
p[s] = value.concat(p[s]); | ||
} | ||
/** | ||
* Splice | ||
@@ -127,3 +118,3 @@ */ | ||
p[s] = _helpers.splice.apply(null, [p[s]].concat(value)); | ||
if (opts.persistent) p[s] = _helpers.splice.apply(null, [p[s]].concat(value));else p[s].splice.apply(p[s], value); | ||
} | ||
@@ -135,3 +126,3 @@ | ||
else if (operationType === 'unset') { | ||
if (_type2['default'].object(p)) delete p[s]; | ||
if (_type2['default'].object(p)) delete p[s];else if (_type2['default'].array(p)) p.splice(s, 1); | ||
} | ||
@@ -145,3 +136,3 @@ | ||
p[s] = (0, _helpers.shallowMerge)({}, p[s], value); | ||
if (opts.persistent) p[s] = (0, _helpers.shallowMerge)({}, p[s], value);else p[s] = (0, _helpers.shallowMerge)(p[s], value); | ||
} | ||
@@ -148,0 +139,0 @@ |
{ | ||
"name": "baobab", | ||
"version": "2.0.0-dev1", | ||
"version": "2.0.0-dev10", | ||
"description": "JavaScript persistent data tree with cursors.", | ||
"main": "./dist/baobab.js", | ||
"dependencies": { | ||
"emmett": "^3.1.0" | ||
"emmett": "^3.1.1" | ||
}, | ||
@@ -9,0 +9,0 @@ "devDependencies": { |
631
README.md
@@ -5,5 +5,5 @@ [![Build Status](https://travis-ci.org/Yomguithereal/baobab.svg)](https://travis-ci.org/Yomguithereal/baobab) | ||
**Baobab** is a JavaScript [persistent](http://en.wikipedia.org/wiki/Persistent_data_structure) and optionally [immutable](http://en.wikipedia.org/wiki/Immutable_object) data tree supporting cursors and enabling developers to easily navigate and monitor nested data. | ||
**Baobab** is a JavaScript [persistent](http://en.wikipedia.org/wiki/Persistent_data_structure) and [immutable](http://en.wikipedia.org/wiki/Immutable_object) (at least by default) data tree supporting cursors and enabling developers to easily navigate and monitor nested data through events. | ||
It is mainly inspired by functional [zippers](http://clojuredocs.org/clojure.zip/zipper) such as Clojure's ones and by [Om](https://github.com/swannodette/om)'s cursors. | ||
It is mainly inspired by functional [zippers](http://clojuredocs.org/clojure.zip/zipper) (such as Clojure's ones) and by [Om](https://github.com/swannodette/om)'s cursors. | ||
@@ -28,8 +28,7 @@ It aims at providing a centralized model holding an application's state and can be paired with **React** easily through mixins, higher order components, wrapper components or decorators (available [there](https://github.com/Yomguithereal/baobab-react)). | ||
* [Polymorphisms](#polymorphisms) | ||
* [Computed data](#computed-data) | ||
* [Specialized getters](#specialized-getters) | ||
* [Traversal](#traversal) | ||
* [Options](#options) | ||
* [Facets](#facets) | ||
* [History](#history) | ||
* [Update specifications](#update-specifications) | ||
* [Chaining mutations](#chaining-mutations) | ||
* [Common pitfalls](#common-pitfalls) | ||
@@ -55,4 +54,6 @@ * [Philosophy](#philosophy) | ||
colorsCursor.on('update', function() { | ||
console.log('Selected colors have updated:', colorsCursor.get()); | ||
colorsCursor.on('update', function(e) { | ||
var eventData = e.data; | ||
console.log('Selected colors have updated:', eventData.data); | ||
}); | ||
@@ -86,3 +87,3 @@ | ||
The library (as a standalone) currently weights ~20ko minified and ~6ko gzipped. | ||
The library (as a standalone) currently weights ~25ko minified and ~7ko gzipped. | ||
@@ -109,3 +110,3 @@ ## Usage | ||
Then you can create cursors to easily access nested data in your tree and be able to listen to changes concerning the part of the tree you selected. | ||
Then you can create cursors to easily access nested data in your tree and listen to changes concerning the part of the tree you selected. | ||
@@ -136,3 +137,3 @@ ```js | ||
// Note you can also perform subselections if needed | ||
// Note that you can also perform subselections if needed | ||
var colorCursor = paletteCursor.select('colors'); | ||
@@ -143,10 +144,8 @@ ``` | ||
A *baobab* tree can obviously be updated. However, one has to understand that the library won't do so, at least by default, synchronously. | ||
A *baobab* tree can obviously be updated. However, one has to understand that, even if you can write the tree synchronously, `update` events won't be, at least by default, fired until next frame. | ||
Rather, the tree will stack and merge every update order you give it and will only commit them later on (note that you remain free to force a synchronous update of the tree through `tree.commit` or by tweaking the tree's [options](#options)). | ||
If you really need to fire an update synchronously (typically if you store a form's state within your app's state, for instance), your remain free to use the `tree.commit` method or tweak the tree's [options](#options) to fit your needs. | ||
This enables the tree to perform efficient mutations and to be able to notify any relevant cursors that the data they are watching over has changed. | ||
**Important**: Note that the tree, being a persistent data structure, will shift the references of the objects it stores in order to enable *immutabley* comparisons between one version of the state and another (this is especially useful when using strategies as such as React's [pure rendering](https://facebook.github.io/react/docs/pure-render-mixin.html)). | ||
**Important**: Note that the tree will shift the references of the objects it stores in order to enable *immutabley* comparisons between one version of the state and another (this is especially useful when using things as such as React's [PureRenderMixin](https://facebook.github.io/react/docs/pure-render-mixin.html)). | ||
*Example* | ||
@@ -164,22 +163,10 @@ | ||
##### Tree level | ||
##### Cursor level | ||
*Setting a key* | ||
Since **Baobab** is immutable by default, note that all the methods below will return the data of the updated node for convenience and so you don't have to use `.get` afterwards to continue what you were doing. | ||
```js | ||
tree.set('hello', 'world'); | ||
``` | ||
*Unsetting a key* | ||
```js | ||
tree.unset('hello'); | ||
``` | ||
##### Cursor level | ||
*Replacing data at cursor* | ||
```js | ||
cursor.set({hello: 'world'}); | ||
var newData = cursor.set({hello: 'world'}); | ||
``` | ||
@@ -190,6 +177,10 @@ | ||
```js | ||
cursor.set('hello', 'world'); | ||
var newData = cursor.set('hello', 'world'); | ||
// Nested path | ||
cursor.set(['one', 'two'], 'world'); | ||
var newData = cursor.set(['one', 'two'], 'world'); | ||
// Same as | ||
var newData = cursor.select('one', 'two').set('world'); | ||
// Or | ||
var newData = cursor.select('one').set('two', 'world'); | ||
``` | ||
@@ -217,12 +208,9 @@ | ||
```js | ||
cursor.push('purple'); | ||
var newArray = cursor.push('purple'); | ||
// Pushing several values | ||
cursor.push(['purple', 'orange']); | ||
// At key | ||
cursor.push('list', 'orange') | ||
var newArray = cursor.push('list', 'orange') | ||
// Nested path | ||
cursor.push(['one', 'two'], 'orange'); | ||
var newArray = cursor.push(['one', 'two'], 'orange'); | ||
``` | ||
@@ -235,12 +223,23 @@ | ||
```js | ||
cursor.unshift('purple'); | ||
var newArray = cursor.unshift('purple'); | ||
// Unshifting several values | ||
cursor.unshift(['purple', 'orange']); | ||
// At key | ||
var newArray = cursor.unshift('list', 'orange') | ||
// Nested path | ||
var newArray = cursor.unshift(['one', 'two'], 'orange'); | ||
``` | ||
*Concatenating* | ||
Obviously this will fail if the value at cursor is not an array. | ||
```js | ||
var newArray = cursor.concat(['purple', 'yellow']); | ||
// At key | ||
cursor.unshift('list', 'orange') | ||
var newArray = cursor.concat('list', ['purple', 'yellow']) | ||
// Nested path | ||
cursor.unshift(['one', 'two'], 'orange'); | ||
var newArray = cursor.concat(['one', 'two'], ['purple', 'yellow']); | ||
``` | ||
@@ -253,12 +252,12 @@ | ||
```js | ||
cursor.splice([1, 1]); | ||
var newArray = cursor.splice([1, 1]); | ||
// Applying splice n times with different arguments | ||
cursor.splice([[1, 2], [3, 2, 'hello']]); | ||
var newArray = cursor.splice([[1, 2], [3, 2, 'hello']]); | ||
// At key | ||
cursor.splice('list', [1, 1]) | ||
var newArray = cursor.splice('list', [1, 1]) | ||
// Nested path | ||
cursor.splice(['one', 'two'], [1, 1]); | ||
var newArray = cursor.splice(['one', 'two'], [1, 1]); | ||
``` | ||
@@ -273,41 +272,50 @@ | ||
cursor.apply(inc); | ||
var newData = cursor.apply(inc); | ||
// At key | ||
cursor.apply('number', inc) | ||
var newData = cursor.apply('number', inc) | ||
// Nested path | ||
cursor.apply(['one', 'two'], inc); | ||
var newData = cursor.apply(['one', 'two'], inc); | ||
``` | ||
*Chaining functions through composition* | ||
*Shallowly merging objects* | ||
For more details about this particular point, check [this](#chaining-mutations). | ||
Obviously this will fail if the value at cursor is not an object. | ||
```js | ||
var inc = function(currentData) { | ||
return currentData + 1; | ||
}; | ||
var newObject = cursor.merge({hello: 'world'}); | ||
cursor.chain(inc); | ||
// At key | ||
cursor.chain('number', inc) | ||
var newObject = cursor.merge('object', {hello: 'world'}) | ||
// Nested path | ||
cursor.chain(['one', 'two'], inc); | ||
var newObject = cursor.merge(['one', 'two'], {hello: 'world'}); | ||
``` | ||
*Shallowly merging objects* | ||
##### Tree level | ||
Obviously this will fail if the value at cursor is not an object. | ||
Note that you can use any of the above methods on the tree itself for convenience: | ||
*Example* | ||
```js | ||
cursor.merge({hello: 'world'}); | ||
// Completely replacing the tree's data | ||
tree.set({hello: 'world'}); | ||
// At key | ||
cursor.merge('object', {hello: 'world'}) | ||
// Setting value at key | ||
tree.set('hello', 'world'); | ||
// Nested path | ||
cursor.merge(['one', 'two'], {hello: 'world'}); | ||
tree.set(['message', 'hello'], 'world'); | ||
// Every other methods also work | ||
tree.set | ||
tree.unset | ||
tree.apply | ||
tree.push | ||
tree.unshift | ||
tree.splice | ||
tree.concat | ||
tree.merge | ||
``` | ||
@@ -317,3 +325,3 @@ | ||
Whenever an update is committed, events are fired to notify relevant parts of the tree that data was changed so that bound elements, React components, for instance, can update. | ||
Whenever an update is committed, events are fired to notify relevant parts of the tree that data was changed so that bound elements, UI components, for instance, may update. | ||
@@ -360,11 +368,25 @@ Note however that **only** relevant cursors will be notified of a change. | ||
Will fire if the tree is updated. | ||
Will fire if the tree is updated (this concerns the asynchronous updates of the tree). | ||
```js | ||
tree.on('update', function(e) { | ||
console.log('Update log', e.data.log); | ||
console.log('Previous data', e.data.previousData); | ||
var eventData = e.data; | ||
console.log('New data:', eventData.data); | ||
console.log('Previous data:', eventData.previousData); | ||
console.log('Transaction details:', eventData.transaction); | ||
console.log('Affected paths', eventData.paths); | ||
}); | ||
``` | ||
*write* | ||
Will fire whenever the tree is written (synchronously, unlike the `update` event). | ||
```js | ||
tree.on('write', function(e) { | ||
console.log('Affected path:', e.data.path); | ||
}); | ||
``` | ||
*invalid* | ||
@@ -387,2 +409,3 @@ | ||
console.log('Path:', e.data.path); | ||
console.log('Solved path:', e.data.solvedPath); | ||
console.log('Target data:', e.data.data); | ||
@@ -413,18 +436,2 @@ }); | ||
*irrelevant* | ||
Will fire if the cursor has become irrelevant and does not watch over any data anymore. | ||
```js | ||
cursor.on('irrelevant', fn); | ||
``` | ||
*relevant* | ||
Will fire if the cursor was irrelevant but becomes relevant again. | ||
```js | ||
cursor.on('relevant', fn); | ||
``` | ||
##### N.B. | ||
@@ -438,3 +445,3 @@ | ||
If you ever need to, know that they are many ways to select and retrieve data within a *baobab*. | ||
If you ever need to, know that there are many ways to select and retrieve data within a *baobab*. | ||
@@ -484,12 +491,4 @@ ```js | ||
// Retrieving or selecting data by using the value of another cursor | ||
var currentColorCursor = paletteCursor.select('colors', {$cursor: ['palette', 'currentColor']}); | ||
var currentColor = paletteCursor.get('colors', {$cursor: ['palette', 'currentColor']}); | ||
// Creating a blank tree | ||
var blankTree = new Baobab(); | ||
// You despise "new"? | ||
var tree = Baobab(); | ||
``` | ||
@@ -499,4 +498,199 @@ | ||
#### Computed data (facets) | ||
For convenience, **Baobab** allows you to store computed data within the tree. | ||
Computed data node can be considered as a "view" or "facet" over some parts of the data stored within your tree (a filtered version of an array, for instance). | ||
Those specific nodes must have, by convention, a key starting with `$` and can define dependencies to some paths within the tree. | ||
*Example* | ||
```js | ||
var tree = new Baobab({ | ||
user: { | ||
name: 'John', | ||
surname: 'Smith', | ||
$fullname: { | ||
cursors: { | ||
name: ['user', 'name'], | ||
surname: ['user', 'surname'] | ||
}, | ||
get: function(data) { | ||
return data.name + ' ' + data.surname; | ||
} | ||
} | ||
}, | ||
data: { | ||
messages: [ | ||
{from: 'John', txt: 'Hey'}, | ||
{from: 'Jack', txt: 'Ho'} | ||
], | ||
$fromJohn: { | ||
cursors: { | ||
messages: ['data', 'messages'], | ||
}, | ||
get: function(data) { | ||
return data.messages.filter(function(m) { | ||
return m.from === 'John'; | ||
}); | ||
} | ||
} | ||
} | ||
}); | ||
// Alternate shorthand definition syntax | ||
var tree = new Baobab({ | ||
user: { | ||
name: 'John', | ||
surname: 'Smith', | ||
$fullname: [ | ||
['user', 'name'], | ||
['user', 'surname'], | ||
function(name, surname) { | ||
return name + ' ' + surname; | ||
} | ||
] | ||
}, | ||
data: { | ||
messages: [ | ||
{from: 'John', txt: 'Hey'}, | ||
{from: 'Jack', txt: 'Ho'} | ||
], | ||
$fromJohn: [ | ||
['data', 'messages'], | ||
function(messages) { | ||
return messages.filter(function(m) { | ||
return m.from === 'John'; | ||
}); | ||
} | ||
] | ||
} | ||
}); | ||
// You can then access or select data naturally | ||
tree.get('user', '$fullname'); | ||
>>> 'John Smith' | ||
tree.get('data', '$fromJohn'); | ||
>>> [{from: 'John', txt: 'Hey'}] | ||
tree.get('data', '$fromJohn', 'txt'); | ||
>>> 'Hey' | ||
// Just note that computed data node is read-only and that the tree | ||
// will throw if you try to update a path lying beyond a computed node | ||
tree.set(['data', '$fromJohn', 'txt'], 'Yay'); | ||
>>> Error! | ||
``` | ||
The computed data node will of course automatically update whenever at least one of the watched paths is updated. | ||
Note that the getter function is lazy and that data won't be computed before you access it. | ||
#### Specialized getters | ||
**tree/cursor.exists** | ||
Check whether a specific path exists within the tree (won't fire a `get` event). | ||
```js | ||
// Probably true | ||
tree.exists(); | ||
// Does the cursor points at an existing path? | ||
cursor.exists(); | ||
// Can also take a path | ||
tree.exists('hello'); | ||
tree.exists('hello', 'message'); | ||
tree.exists(['hello', 'message']); | ||
``` | ||
**tree/cursor.serialize** | ||
Retrieve only raw data (therefore avoiding computed data) from the tree or a cursor. | ||
This is useful when you want to serialize your tree into JSON, for instance. | ||
```js | ||
tree.serialize(); | ||
cursor.serialize(); | ||
// Can also take a path | ||
tree.serialize('hello'); | ||
tree.serialize('hello', 'message'); | ||
tree.serialize(['hello', 'message']); | ||
``` | ||
**tree.watch** | ||
Create a watcher that will fire an `update` event if any of the given paths is affected by a transaction. | ||
This is useful to create modules binding a state tree to UI components. | ||
```js | ||
// Considering the following tree | ||
var tree = new Baobab({ | ||
one: { | ||
name: 'John' | ||
}, | ||
two: { | ||
surname: 'Smith' | ||
} | ||
}); | ||
var watcher = tree.watch({ | ||
name: ['one', 'name'], | ||
surname: ['two', 'surname'] | ||
}); | ||
watcher.on('update', function(e) { | ||
// One of the watched paths was updated! | ||
}); | ||
``` | ||
**tree/cursor.project** | ||
Retrieve data from several parts of the tree by following the given projection: | ||
```js | ||
// Considering the following tree | ||
var tree = new Baobab({ | ||
one: { | ||
name: 'John' | ||
}, | ||
two: { | ||
surname: 'Smith' | ||
} | ||
}); | ||
// Using an object projection | ||
tree.project({ | ||
name: ['one', 'name'], | ||
surname: ['two', 'surname'] | ||
}); | ||
>>> {name: 'John', surname: 'Smith'} | ||
// Using an array projection | ||
tree.project([ | ||
['one', 'name'], | ||
['two', 'surname'] | ||
]); | ||
>>> ['John', 'Smith'] | ||
``` | ||
#### Traversal | ||
*Getting root cursor* | ||
```js | ||
var tree = new Baobab({first: {second: 'yeah'}}), | ||
cursor = tree.select('first'); | ||
var rootCursor = tree.root; | ||
// or | ||
var rootCursor = cursor.root(); | ||
``` | ||
*Going up in the tree* | ||
@@ -525,3 +719,6 @@ | ||
listCursor.select(1).down().left().get(); | ||
listCursor.select(1).down().right().get(); | ||
>>> 4 | ||
listCursor.select(1).down().right().left().get(); | ||
>>> 3 | ||
@@ -536,16 +733,5 @@ | ||
*Getting root cursor* | ||
*Getting information about the cursor's location in the tree* | ||
```js | ||
var tree = new Baobab({first: {second: 'yeah'}}), | ||
cursor = tree.select('first'); | ||
var rootCursor = tree.root; | ||
// or | ||
var rootCursor = cursor.root(); | ||
``` | ||
*Checking information about the cursor's location in the tree* | ||
```js | ||
cursor.isRoot(); | ||
@@ -580,5 +766,4 @@ cursor.isBranch(); | ||
* **asynchronous** *boolean* [`true`]: should the tree delay the update to the next frame or fire them synchronously? | ||
* **facets** *object*: a collection of facets to register when the tree is istantiated. For more information, see [facets](#facets). | ||
* **immutable** *boolean* [`false`]: should the tree's data be immutable? Note that immutability is performed through `Object.freeze`. | ||
* **syncwrite** *boolean* [`false`]: when in syncwrite mode, all writes will apply to the tree synchronously, so you can easily read your writes, while keeping update events asynchronous. | ||
* **immutable** *boolean* [`true`]: should the tree's data be immutable? Note that immutability is performed through `Object.freeze` and should be disabled in production for performance reasons. | ||
* **persistent** *boolean* [`true`]: should the tree be persistent. Know that disabling this option, while bringing a significant performance boost on heavy data, will make you lose the benefits of your tree's history and `O(1)` comparisons of objects. | ||
* **validate** *function*: a function in charge of validating the tree whenever it updates. See below for an example of such function. | ||
@@ -600,93 +785,5 @@ * **validationBehavior** *string* [`rollback`]: validation behavior of the tree. If `rollback`, the tree won't apply the current update and fire an `invalid` event while `notify` will only emit the event and let the tree enter the invalid state anyway. | ||
#### Facets | ||
Facets can be considered as a "view" on the data of your tree (a filtered version of an array stored within your tree, for instance). | ||
They watch over some paths of your tree and will update their cached data only when needed. As for cursors, you can also listen to their updates. | ||
Facets can be defined at the tree's instantiation likewise: | ||
```js | ||
var tree = new Baobab( | ||
// Data | ||
{ | ||
projects: [ | ||
{ | ||
id: 1, | ||
name: 'Tezcatlipoca', | ||
user: 'John' | ||
}, | ||
{ | ||
id: 2, | ||
name: 'Huitzilopochtli', | ||
user: 'John' | ||
}, | ||
{ | ||
id: 3, | ||
name: 'Tlaloc', | ||
user: 'Jack' | ||
} | ||
], | ||
currentProjectId: 1 | ||
}, | ||
// Options | ||
{ | ||
facets: { | ||
// Name of your facet | ||
currentProject: { | ||
// Cursors bound to your facet | ||
// If any of the paths listed below fire | ||
// an update, so will the facet. | ||
cursors: { | ||
id: ['currentProjectId'], | ||
projects: ['projects'] | ||
}, | ||
get: function(data) { | ||
// 'data' is the value of your mapped cursors | ||
// Just return the wanted value | ||
// Here, we use lodash to return the current's project | ||
// data based on its id | ||
return _.find(data.projects, {id: data.id}); | ||
} | ||
}, | ||
// Other example | ||
filteredProjects: { | ||
cursors: { | ||
projects: ['projects'] | ||
}, | ||
get: function(data) { | ||
return data.projects.filter(function(p) { | ||
return p.user === 'John'; | ||
}); | ||
} | ||
}, | ||
} | ||
} | ||
) | ||
``` | ||
You can then access facets' data and listen to their changes thusly: | ||
```js | ||
var facet = tree.facets.currentProject; | ||
// Getting value (cached and only computed if needed) | ||
facet.get(); | ||
// Facets are also event emitters | ||
facet.on('update', function() { | ||
console.log('New value:', facet.get()); | ||
}); | ||
``` | ||
#### History | ||
**Baobab** lets you record the state of any cursor so you can seamlessly implement undo/redo features. | ||
**Baobab** lets you record the successive states of any cursor so you can seamlessly implement undo/redo features. | ||
@@ -721,3 +818,3 @@ *Example* | ||
Default max number of records is 5. | ||
If you do not provide a maximum number of records, will record everything without any limit. | ||
@@ -753,8 +850,2 @@ ```js | ||
*Checking whether the cursor is currently recording* | ||
```js | ||
cursor.recording; | ||
``` | ||
*Retrieving the cursor's history* | ||
@@ -766,109 +857,4 @@ | ||
#### Update specifications | ||
If you ever need to specify complex updates without replacing the whole subtree you are acting on, for readability or performance reasons, you remain free to use **Baobab**'s internal update specifications. | ||
Those are widely inspired by React's immutable [helpers](http://facebook.github.io/react/docs/update.html) and can be used through `tree.update` or `cursor.update`. | ||
**Specifications** | ||
Those specifications are described by a JavaScript object that follows the nested structure you are trying to update and applying dollar-prefixed commands at leaf level. | ||
The available commands are the following and are basically the same as the cursor's updating methods: | ||
* `$set` | ||
* `$apply` | ||
* `$chain` | ||
* `$push` | ||
* `$unshift` | ||
* `$splice` | ||
* `$merge` | ||
* `$unset` | ||
*Example* | ||
```js | ||
var tree = new Baobab({ | ||
users: { | ||
john: { | ||
firstname: 'John', | ||
lastname: 'Silver' | ||
}, | ||
jack: { | ||
firstname: 'Jack', | ||
lastname: 'Gold' | ||
} | ||
} | ||
}); | ||
// From tree | ||
tree.update({ | ||
users: { | ||
john: { | ||
firstname: { | ||
$set: 'John the 3rd' | ||
} | ||
}, | ||
jack: { | ||
firstname: { | ||
$apply: function(firstname) { | ||
return firstname + ' the 2nd'; | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
// From cursor | ||
var cursor = tree.select('users', 'john'); | ||
cursor.update({ | ||
firstname: { | ||
$set: 'Little Johnsie' | ||
} | ||
}) | ||
``` | ||
#### Chaining mutations | ||
Because updates will be committed later, update orders are merged when given and the new order will sometimes override older ones, especially if you set the same key twice to different values. | ||
This is problematic when what you want is to increment a counter for instance. In those cases, you need to *chain* functions that will be assembled through composition when the update orders are merged. | ||
```js | ||
var inc = function(i) { | ||
return i + 1; | ||
}; | ||
// If cursor.get() >>> 1 | ||
cursor.apply(inc); | ||
cursor.apply(inc); | ||
// will produce 2, while | ||
cursor.chain(inc); | ||
cursor.chain(inc); | ||
// will produce 3 | ||
``` | ||
#### Common pitfalls | ||
**Immutable behaviour** | ||
TL;DR: Don't mutate things in your *baobab* tree. Let the tree handle its own mutations. | ||
For performance and size reasons *baobab* does not (yet?) use an immutable data structure. However, because it aims at producing a one-way data flow for your application state (like **React** would at component level), it must be used like an immutable data structure. | ||
For this reason, don't be surprised if you mutate things and break your tree. | ||
```js | ||
// This is bad: | ||
var users = tree.get('users'); | ||
users[0].name = 'Jonathan'; | ||
// This is also bad: | ||
var o = {hello: 'world'}; | ||
tree.set('key', o); | ||
o.hello = 'other world'; | ||
``` | ||
Note that, if you want the tree to be immutable, you can now enable it through the `immutable` [option](#options). | ||
**Releasing** | ||
@@ -878,3 +864,3 @@ | ||
Thus, any Baobab object can be cleared from memory by using the `release` method. This applies to trees, cursors and facets. | ||
Thus, any tree or cursor object can be cleared from memory by using the `release` method. | ||
@@ -884,14 +870,13 @@ ```js | ||
cursor.release(); | ||
facet.release(); | ||
``` | ||
Note also that releasing a tree will consequently and automatically release every of its cursors and facets. | ||
Note also that releasing a tree will consequently and automatically release every of its cursors and computed data nodes. | ||
## Philosophy | ||
**UIs as pure functions** | ||
**User interfaces as pure functions** | ||
UIs should be, as far as possible, considered as pure functions. Baobab is just a way to provide the needed arguments, i.e. the data representing your app's state, to such a function. | ||
User interfacess should be, as far as possible, considered as pure functions. Baobab is just a way to provide the needed arguments, i.e. the data representing your app's state, to such a function. | ||
Considering your UIs like pure functions comes along with collateral advantages like easy undo/redo features, state storing (just save your tree in the `localStorage` and here you go) and easy isomorphism. | ||
Considering your UIs like pure functions comes along with collateral advantages like easy undo/redo features, state storing (just save your tree in the `localStorage` and here you go) and easy usage in both client & server. | ||
@@ -906,4 +891,14 @@ **Only data should enter the tree** | ||
**From v0.4.x to 1.0.0** | ||
**From v1 to v2** | ||
* The tree is now immutable by default (but you can shunt this behavior through a dedicated [option](#options)). | ||
* Writing to the tree is now synchronous for convenience. Updates remain asynchronous for obvious performance reasons. | ||
* You cannot chain update methods now since those will return the affected node's data to better tackle immutability. | ||
* The strange concat-like behavior of the `push` and `unshift` method was dropped in favor of the `concat` method. | ||
* Facets are now full-fledged computed data node sitting within the tree itself. | ||
* The weird `$cursor` sugar has now been dropped. | ||
* The update specifications have been dropped. | ||
**From v0.4.x to v1** | ||
A lot of changes occurred between `0.4.x` and `1.0.0`. Most notable changes being the following ones: | ||
@@ -910,0 +905,0 @@ |
@@ -15,2 +15,3 @@ /** | ||
deepFreeze, | ||
getIn, | ||
makeError, | ||
@@ -20,3 +21,2 @@ deepMerge, | ||
shallowMerge, | ||
solvePath, | ||
uniqid | ||
@@ -39,2 +39,5 @@ } from './helpers'; | ||
// Should the tree be persistent? | ||
persistent: true, | ||
// Validation specifications | ||
@@ -91,2 +94,6 @@ validate: null, | ||
// Disabling immutability if persistence if disabled | ||
if (!this.options.persistent) | ||
this.options.immutable = false; | ||
// Privates | ||
@@ -119,6 +126,6 @@ this._identity = '[object Baobab]'; | ||
[ | ||
'append', | ||
'apply', | ||
'concat', | ||
'exists', | ||
'get', | ||
'prepend', | ||
'push', | ||
@@ -142,8 +149,6 @@ 'merge', | ||
* | ||
* @param {array} [path] - Path to the modified node. | ||
* @param {string} [operation] - Type of the applied operation. | ||
* @param {mixed} [node] - The new node. | ||
* @return {Baobab} - The tree itself for chaining purposes. | ||
* @param {array} [path] - Path to the modified node. | ||
* @return {Baobab} - The tree itself for chaining purposes. | ||
*/ | ||
_refreshComputedDataIndex(path, operation, node) { | ||
_refreshComputedDataIndex(path) { | ||
@@ -180,3 +185,3 @@ // Refreshing the whole tree | ||
if (!path) { | ||
if (!path || !path.length) { | ||
@@ -186,2 +191,10 @@ // Walk the whole tree | ||
} | ||
else { | ||
// Retrieving parent of affected node | ||
const parentNode = getIn(this.data, path.slice(0, -1)).data; | ||
// Walk the affected leaf | ||
return walk(parentNode, path.slice(0, -1)); | ||
} | ||
} | ||
@@ -226,3 +239,3 @@ | ||
if (!cursor) { | ||
cursor = new Cursor(this, path, hash); | ||
cursor = new Cursor(this, path, {hash}); | ||
this._cursors[hash] = cursor; | ||
@@ -257,9 +270,9 @@ } | ||
// Stashing previous data if this is the frame's first update | ||
if (!this._transaction.length) | ||
this.previousData = this.data; | ||
// Solving the given path | ||
const {solvedPath, exists} = getIn( | ||
this.data, | ||
path, | ||
this._computedDataIndex | ||
); | ||
// Applying the operation | ||
const solvedPath = solvePath(this.data, path); | ||
// If we couldn't solve the path, we throw | ||
@@ -271,4 +284,13 @@ if (!solvedPath) | ||
// We don't unset irrelevant paths | ||
if (operation.type === 'unset' && !exists) | ||
return; | ||
// Stashing previous data if this is the frame's first update | ||
if (!this._transaction.length) | ||
this.previousData = this.data; | ||
const hash = hashPath(solvedPath); | ||
// Applying the operation | ||
const {data, node} = update( | ||
@@ -286,2 +308,6 @@ this.data, | ||
// Refreshing facet index | ||
// TODO: provide a setting to disable this or at least selectively for perf | ||
this._refreshComputedDataIndex(solvedPath); | ||
// Emitting a `write` event | ||
@@ -291,5 +317,4 @@ this.emit('write', {path: solvedPath}); | ||
// Should we let the user commit? | ||
if (!this.options.autoCommit) { | ||
if (!this.options.autoCommit) | ||
return node; | ||
} | ||
@@ -371,2 +396,16 @@ // Should we update asynchronously? | ||
/** | ||
* Method used to watch a collection of paths within the tree. Very useful | ||
* to bind UI components and such to the tree. | ||
* | ||
* @param {object|array} paths - Paths to listen. | ||
* @return {Cursor} - A special cursor that can be listened. | ||
*/ | ||
watch(paths) { | ||
if (!type.object(paths) && !type.array(paths)) | ||
throw Error('Baobab.watch: wrong argument.'); | ||
return new Cursor(this, null, {watched: paths}); | ||
} | ||
/** | ||
* Method releasing the tree and its attached data from memory. | ||
@@ -413,3 +452,3 @@ */ | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '2.0.0-dev1' | ||
value: '2.0.0-dev10' | ||
}); | ||
@@ -416,0 +455,0 @@ |
@@ -16,16 +16,39 @@ /** | ||
makeError, | ||
solvePath, | ||
shallowClone, | ||
solveUpdate | ||
} from './helpers'; | ||
/** | ||
* Traversal helper function for dynamic cursors. Will throw a legible error | ||
* if traversal is not possible. | ||
* | ||
* @param {string} method - The method name, to create a correct error msg. | ||
* @param {array} solvedPath - The cursor's solved path. | ||
*/ | ||
function checkPossibilityOfDynamicTraversal(method, solvedPath) { | ||
if (!solvedPath) | ||
throw makeError( | ||
`Baobab.Cursor.${method}: ` + | ||
`cannot use ${method} on an unresolved dynamic path.`, | ||
{path: solvedPath} | ||
); | ||
} | ||
/** | ||
* Cursor class | ||
* | ||
* Note: opts.watched is not called opts.watch not to tamper with experimental | ||
* `Object.prototype.watch`. | ||
* | ||
* @constructor | ||
* @param {Baobab} tree - The cursor's root. | ||
* @param {array} path - The cursor's path in the tree. | ||
* @param {string} hash - The path's hash computed ahead by the tree. | ||
* @param {Baobab} tree - The cursor's root. | ||
* @param {array} path - The cursor's path in the tree. | ||
* @param {object} [opts] - Options | ||
* @param {string} [opts.hash] - The path's hash computed ahead by the tree. | ||
* @param {array} [opts.watched] - Parts of the tree the cursor is meant to | ||
* watch over. | ||
*/ | ||
export default class Cursor extends Emitter { | ||
constructor(tree, path, hash) { | ||
constructor(tree, path, opts={}) { | ||
super(); | ||
@@ -43,6 +66,7 @@ | ||
this.path = path; | ||
this.hash = hash; | ||
this.hash = opts.hash; | ||
// State | ||
this.state = { | ||
killed: false, | ||
recording: false, | ||
@@ -52,2 +76,20 @@ undoing: false | ||
// Checking whether the cursor is a watcher | ||
if (opts.watched) { | ||
this._watched = shallowClone(opts.watched); | ||
// Normalizing path | ||
for (let k in this._watched) | ||
if (this._watched[k] instanceof Cursor) | ||
this._watched[k] = this._watched[k].path; | ||
// Keeping track of watched paths | ||
this._watchedPaths = opts.watched && (!type.array(this._watched) ? | ||
Object.keys(this._watched).map(k => this._watched[k]) : | ||
this._watched); | ||
// Overriding the cursor's get method | ||
this.get = this.tree.project.bind(this.tree, this._watched); | ||
} | ||
// Checking whether the given path is dynamic or not | ||
@@ -62,5 +104,19 @@ this._dynamicPath = type.dynamicPath(this.path); | ||
else | ||
this.solvedPath = solvePath(this.tree.data, this.path); | ||
this.solvedPath = this._getIn(this.path).solvedPath; | ||
/** | ||
* Listener bound to the tree's writes so that cursors with dynamic paths | ||
* may update their solved path correctly. | ||
* | ||
* @param {object} event - The event fired by the tree. | ||
*/ | ||
this._writeHandler = ({data}) => { | ||
if (this.state.killed || | ||
!solveUpdate([data.path], this._getComparedPaths())) | ||
return; | ||
this.solvedPath = this._getIn(this.path).solvedPath; | ||
}; | ||
/** | ||
* Function in charge of actually trigger the cursor's updates and | ||
@@ -72,4 +128,8 @@ * deal with the archived records. | ||
const fireUpdate = (previousData) => { | ||
const record = getIn(previousData, this.solvedPath); | ||
if (this._watched) | ||
return this.emit('update'); | ||
const record = getIn(previousData, this.solvedPath).data; | ||
if (this.state.recording && !this.state.undoing) | ||
@@ -97,29 +157,11 @@ this.archive.add(record); | ||
this._updateHandler = (event) => { | ||
if (this.state.killed) | ||
return; | ||
const {paths, previousData} = event.data, | ||
update = fireUpdate.bind(this, previousData); | ||
update = fireUpdate.bind(this, previousData), | ||
comparedPaths = this._getComparedPaths(); | ||
// Checking whether we should keep track of some dependencies | ||
// TODO: some operations here might be merged for perfs | ||
const additionalPaths = this._facetPath ? | ||
getIn(this.tree._computedDataIndex, this._facetPath).paths : | ||
[]; | ||
// If this is the root selector, we fire already | ||
if (this.isRoot()) | ||
if (solveUpdate(paths, comparedPaths)) | ||
return update(); | ||
// If the cursor's path is dynamic, we need to recompute it | ||
if (this._dynamicPath) | ||
this.solvedPath = solvePath(this.tree.data, this.path); | ||
let shouldFire = false; | ||
if (this.solvedPath) | ||
shouldFire = solveUpdate( | ||
paths, | ||
[this.solvedPath].concat(additionalPaths) | ||
); | ||
if (shouldFire) | ||
return update(); | ||
}; | ||
@@ -134,2 +176,6 @@ | ||
bound = true; | ||
if (this._dynamicPath) | ||
this.tree.on('write', this._writeHandler); | ||
return this.tree.on('update', this._updateHandler); | ||
@@ -139,3 +185,2 @@ }; | ||
// If the path is dynamic, we actually need to listen to the tree | ||
// TODO: there should be another way | ||
if (this._dynamicPath) { | ||
@@ -153,2 +198,67 @@ this._lazyBind(); | ||
/** | ||
* Internal helpers | ||
* ----------------- | ||
*/ | ||
/** | ||
* Curried version of the `getIn` helper and ready to serve a cursor instance | ||
* purpose without having to write endlessly the same args over and over. | ||
* | ||
* @param {array} path - The path to get in the tree. | ||
* @return {object} - The result of the `getIn` helper. | ||
*/ | ||
_getIn(path) { | ||
return getIn( | ||
this.tree.data, | ||
path, | ||
this.tree._computedDataIndex, | ||
this.tree.options | ||
); | ||
} | ||
/** | ||
* Method returning the paths of the tree watched over by the cursor and that | ||
* should be taken into account when solving a potential update. | ||
* | ||
* @return {array} - Array of paths to compare with a given update. | ||
*/ | ||
_getComparedPaths() { | ||
let comparedPaths; | ||
// Standard cursor | ||
if (!this._watched) { | ||
// Checking whether we should keep track of some dependencies | ||
const additionalPaths = this._facetPath ? | ||
getIn(this.tree._computedDataIndex, this._facetPath) | ||
.data | ||
.relatedPaths() : | ||
[]; | ||
comparedPaths = [this.solvedPath].concat(additionalPaths); | ||
} | ||
// Watcher cursor | ||
else { | ||
comparedPaths = this._watchedPaths.reduce((cp, p) => { | ||
if (type.dynamicPath(p)) | ||
p = this._getIn(p).solvedPath; | ||
if (!p) | ||
return cp; | ||
const facetPath = type.facetPath(p); | ||
if (facetPath) | ||
return cp.concat( | ||
getIn(this.tree._computedDataIndex, p).data.relatedPaths()); | ||
return cp.concat([p]); | ||
}, []); | ||
} | ||
return comparedPaths; | ||
} | ||
/** | ||
* Predicates | ||
@@ -224,3 +334,3 @@ * ----------- | ||
up() { | ||
if (this.solvedPath && !this.isRoot()) | ||
if (!this.isRoot()) | ||
return this.tree.select(this.path.slice(0, -1)); | ||
@@ -237,2 +347,4 @@ else | ||
down() { | ||
checkPossibilityOfDynamicTraversal('down', this.solvedPath); | ||
if (!(this._get().data instanceof Array)) | ||
@@ -251,2 +363,4 @@ throw Error('Baobab.Cursor.down: cannot go down on a non-list type.'); | ||
left() { | ||
checkPossibilityOfDynamicTraversal('left', this.solvedPath); | ||
const last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -269,2 +383,4 @@ | ||
right() { | ||
checkPossibilityOfDynamicTraversal('right', this.solvedPath); | ||
const last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -288,2 +404,4 @@ | ||
leftmost() { | ||
checkPossibilityOfDynamicTraversal('leftmost', this.solvedPath); | ||
const last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -304,2 +422,4 @@ | ||
rightmost() { | ||
checkPossibilityOfDynamicTraversal('rightmost', this.solvedPath); | ||
const last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -325,2 +445,4 @@ | ||
map(fn, scope) { | ||
checkPossibilityOfDynamicTraversal('map', this.solvedPath); | ||
let array = this._get().data, | ||
@@ -358,20 +480,38 @@ l = arguments.length; | ||
_get(path=[]) { | ||
const fullPath = this.solvedPath.concat(path), | ||
data = getIn( | ||
this.tree.data, | ||
fullPath, | ||
this.tree._computedDataIndex, | ||
this.tree.options | ||
); | ||
return {data, solvedPath: fullPath}; | ||
if (!type.path(path)) | ||
throw makeError('Baobab.Cursor.getters: invalid path.', {path}); | ||
if (!this.solvedPath) | ||
return {data: undefined, solvedPath: null, exists: false}; | ||
return this._getIn(this.solvedPath.concat(path)); | ||
} | ||
/** | ||
* Method used to check whether a certain path exists in the tree starting | ||
* from the current cursor. | ||
* | ||
* Arity (1): | ||
* @param {path} path - Path to check in the tree. | ||
* | ||
* Arity (2): | ||
* @param {..step} path - Path to check in the tree. | ||
* | ||
* @return {boolean} - Does the given path exists? | ||
*/ | ||
exists(path) { | ||
path = path || path === 0 ? path : []; | ||
if (arguments.length > 1) | ||
path = arrayFrom(arguments); | ||
return this._get(path).exists; | ||
} | ||
/** | ||
* Method used to get data from the tree. Will fire a `get` event from the | ||
* tree so that the user may sometimes react upon it to fecth data, for | ||
* tree so that the user may sometimes react upon it to fetch data, for | ||
* instance. | ||
* | ||
* @todo: check path validity | ||
* | ||
* Arity (1): | ||
@@ -381,3 +521,3 @@ * @param {path} path - Path to get in the tree. | ||
* Arity (2): | ||
* @param {..step} path - Path to get in the tree. | ||
* @param {..step} path - Path to get in the tree. | ||
* | ||
@@ -395,3 +535,3 @@ * @return {mixed} - Data at path. | ||
// Emitting the event | ||
this.tree.emit('get', {data, path: solvedPath}); | ||
this.tree.emit('get', {data, solvedPath, path: this.path.concat(path)}); | ||
@@ -409,3 +549,3 @@ return data; | ||
* Arity (2): | ||
* @param {..step} path - Path to serialize in the tree. | ||
* @param {..step} path - Path to serialize in the tree. | ||
* | ||
@@ -550,7 +690,11 @@ * @return {mixed} - The retrieved raw data. | ||
// Removing listener on parent | ||
// Removing listeners on parent | ||
if (this._dynamicPath) | ||
this.tree.off('write', this._writeHandler); | ||
this.tree.off('update', this._updateHandler); | ||
// Unsubscribe from the parent | ||
delete this.tree._cursors[this.hash]; | ||
if (this.hash) | ||
delete this.tree._cursors[this.hash]; | ||
@@ -565,2 +709,3 @@ // Dereferencing | ||
this.kill(); | ||
this.state.killed = true; | ||
} | ||
@@ -612,2 +757,8 @@ | ||
* | ||
* Note: this is not really possible to make those setters variadic because | ||
* it would create an impossible polymorphism with path. | ||
* | ||
* @todo: perform value validation elsewhere so that tree.update can | ||
* beneficiate from it. | ||
* | ||
* Arity (1): | ||
@@ -672,6 +823,5 @@ * @param {mixed} value - New value to set at cursor's path. | ||
makeSetter('push'); | ||
makeSetter('append', type.array); | ||
makeSetter('concat', type.array); | ||
makeSetter('unshift'); | ||
makeSetter('prepend', type.array); | ||
makeSetter('splice', type.array); | ||
makeSetter('merge', type.object); |
@@ -10,2 +10,3 @@ /** | ||
deepFreeze, | ||
getIn, | ||
makeError, | ||
@@ -20,7 +21,7 @@ solveUpdate | ||
* @param {Baobab} tree - The tree. | ||
* @param {array} path - Path where the facets stands in its tree. | ||
* @param {array} pathInTree - Path where the facets stands in its tree. | ||
* @param {array|object} definition - The facet's definition. | ||
*/ | ||
export default class Facet { | ||
constructor(tree, path, definition) { | ||
constructor(tree, pathInTree, definition) { | ||
@@ -34,4 +35,4 @@ // Checking definition's type | ||
'Baobab.Facet: attempting to create a computed data node with a ' + | ||
`wrong definition (path: /${path.join('/')}).`, | ||
{path, definition} | ||
`wrong definition (path: /${pathInTree.join('/')}).`, | ||
{path: pathInTree, definition} | ||
); | ||
@@ -44,2 +45,7 @@ | ||
// State | ||
this.state = { | ||
killed: false | ||
}; | ||
// Harmonizing | ||
@@ -58,2 +64,7 @@ if (definitionType === 'object') { | ||
this._hasDynamicPaths = this.paths.some(p => type.dynamicPath(p)); | ||
// Is the facet recursive? | ||
this.isRecursive = this.paths.some(p => !!type.facetPath(p)); | ||
// Internal state | ||
@@ -72,5 +83,7 @@ this.state = { | ||
this.listener = ({data: {path}}) => { | ||
if (this.state.killed) | ||
return; | ||
// Is this facet affected by the current write event? | ||
const concerned = solveUpdate([path], this.paths); | ||
const concerned = solveUpdate([path], this.relatedPaths()); | ||
@@ -88,2 +101,33 @@ if (concerned) { | ||
/** | ||
* Method returning solved paths related to the facet. | ||
* | ||
* @return {array} - An array of related paths. | ||
*/ | ||
relatedPaths() { | ||
let paths; | ||
if (this._hasDynamicPaths) | ||
paths = this.paths.map( | ||
p => getIn(this.tree.data, p, this.tree._computedDataIndex).solvedPath | ||
); | ||
else | ||
paths = this.paths; | ||
if (!this.isRecursive) | ||
return paths; | ||
else | ||
return paths.reduce((accumulatedPaths, path) => { | ||
const facetPath = type.facetPath(path); | ||
if (!facetPath) | ||
return accumulatedPaths.concat(path); | ||
// Solving recursive path | ||
const relatedFacet = getIn(this.tree._computedDataIndex, facetPath).data; | ||
return accumulatedPaths.concat(relatedFacet.relatedPaths()); | ||
}, []); | ||
} | ||
/** | ||
* Getter method | ||
@@ -130,3 +174,4 @@ * | ||
this.tree.off('write', this.listener); | ||
this.state.killed = true; | ||
} | ||
} |
@@ -7,2 +7,3 @@ /** | ||
*/ | ||
import Facet from './facet'; | ||
import type from './type'; | ||
@@ -218,19 +219,2 @@ | ||
/** | ||
* Function returning the first element of a list matching the given | ||
* predicate. | ||
* | ||
* @param {array} a - The target array. | ||
* @param {function} fn - The predicate function. | ||
* @return {mixed} - The first matching item or `undefined`. | ||
*/ | ||
function first(a, fn) { | ||
let i, l; | ||
for (i = 0, l = a.length; i < l; i++) { | ||
if (fn(a[i])) | ||
return a[i]; | ||
} | ||
return; | ||
} | ||
/** | ||
* Function freezing the given variable if possible. | ||
@@ -291,2 +275,36 @@ * | ||
/** | ||
* Function used to solve a computed data mask by recursively walking a tree | ||
* and patching it. | ||
* | ||
* @param {boolean} immutable - Is the data immutable? | ||
* @param {mixed} data - Data to patch. | ||
* @param {object} mask - Computed data mask. | ||
* @param {object} [parent] - Parent object in the iteration. | ||
* @param {string} [lastKey] - Current value's key in parent. | ||
*/ | ||
function solveMask(immutable, data, mask, parent) { | ||
for (let k in mask) { | ||
if (k[0] === '$') { | ||
// Patching | ||
data[k] = mask[k].get(); | ||
if (immutable) | ||
deepFreeze(parent); | ||
} | ||
else { | ||
if (immutable) { | ||
data[k] = shallowClone(data[k]); | ||
if (parent) | ||
freeze(parent); | ||
} | ||
solveMask(immutable, data[k], mask[k], data); | ||
} | ||
} | ||
return data; | ||
} | ||
/** | ||
* Function retrieving nested data within the given object and according to | ||
@@ -301,9 +319,16 @@ * the given path. | ||
* @param {object} [mask] - An optional computed data index. | ||
* @return {mixed} - The data at path, or if not found, `undefined`. | ||
* @return {object} result - The result. | ||
* @return {mixed} result.data - The data at path, or `undefined`. | ||
* @return {array} result.solvedPath - The solved path or `null`. | ||
*/ | ||
const notFoundObject = {data: undefined, solvedPath: null, exists: false}; | ||
export function getIn(object, path, mask=null, opts={}) { | ||
path = path || []; | ||
if (!path) | ||
return notFoundObject; | ||
let c = object, | ||
let solvedPath = [], | ||
c = object, | ||
cm = mask, | ||
idx, | ||
i, | ||
@@ -314,17 +339,28 @@ l; | ||
if (!c) | ||
return; | ||
return {data: undefined, solvedPath: path, exists: false}; | ||
if (typeof path[i] === 'function') { | ||
if (!type.array(c)) | ||
return; | ||
return notFoundObject; | ||
c = first(c, path[i]); | ||
idx = index(c, path[i]); | ||
if (!~idx) | ||
return notFoundObject; | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} | ||
else if (typeof path[i] === 'object') { | ||
if (!type.array(c)) | ||
return; | ||
return notFoundObject; | ||
c = first(c, e => compare(e, path[i])); | ||
idx = index(c, e => compare(e, path[i])); | ||
if (!~idx) | ||
return notFoundObject; | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} | ||
else { | ||
solvedPath.push(path[i]); | ||
@@ -346,28 +382,9 @@ // Solving data from a facet if needed | ||
// If the mask is still relevant, we continue until we solved computed data | ||
// completely | ||
// If the mask is still relevant, we solve it down to the leaves | ||
if (cm && Object.keys(cm).length) { | ||
// TODO: optimize, this is hardly performant | ||
c = deepClone(c); | ||
const walk = (d, m) => { | ||
for (let k in m) { | ||
if (k[0] === '$') { | ||
d[k] = m[k].get(); | ||
} | ||
else { | ||
walk(d[k], m[k]); | ||
} | ||
} | ||
}; | ||
walk(c, cm); | ||
// Freezing again if immutable | ||
if (opts.immutable) | ||
deepFreeze(c); | ||
let patchedData = solveMask(opts.immutable, {root: c}, {root: cm}); | ||
c = patchedData.root; | ||
} | ||
return c; | ||
return {data: c, solvedPath, exists: c !== undefined}; | ||
} | ||
@@ -414,2 +431,4 @@ | ||
* be used by Baobab's internal and would be unsuited in any other case. | ||
* Note 4): this function will release any facet found on its path to the | ||
* leaves for cleanup reasons. | ||
* | ||
@@ -437,2 +456,7 @@ * @param {boolean} deep - Whether the merge should be deep or not. | ||
else { | ||
// Releasing | ||
if (o[k] instanceof Facet) | ||
o[k].release(); | ||
o[k] = t[k]; | ||
@@ -497,45 +521,2 @@ } | ||
/** | ||
* Function solving the given path within the target object. | ||
* | ||
* @param {object} object - The object in which the path must be solved. | ||
* @param {array} path - The path to follow. | ||
* @return {mixed} - The solved path if possible, else `null`. | ||
*/ | ||
export function solvePath(object, path) { | ||
let solvedPath = [], | ||
c = object, | ||
idx, | ||
i, | ||
l; | ||
for (i = 0, l = path.length; i < l; i++) { | ||
if (!c) | ||
return null; | ||
if (typeof path[i] === 'function') { | ||
if (!type.array(c)) | ||
return; | ||
idx = index(c, path[i]); | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} | ||
else if (typeof path[i] === 'object') { | ||
if (!type.array(c)) | ||
return; | ||
idx = index(c, e => compare(e, path[i])); | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} | ||
else { | ||
solvedPath.push(path[i]); | ||
c = c[path[i]] || {}; | ||
} | ||
} | ||
return solvedPath; | ||
} | ||
/** | ||
* Function determining whether some paths in the tree were affected by some | ||
@@ -570,3 +551,3 @@ * updates that occurred at the given paths. This helper is mainly used at | ||
if (!c.length) | ||
if (!c || !c.length) | ||
return true; | ||
@@ -579,2 +560,3 @@ | ||
// If path is not relevant, we break | ||
// NOTE: the '!=' instead of '!==' is required here! | ||
if (s != p[k]) | ||
@@ -581,0 +563,0 @@ break; |
@@ -200,4 +200,3 @@ /** | ||
'unshift', | ||
'append', | ||
'prepend', | ||
'concat', | ||
'merge', | ||
@@ -204,0 +203,0 @@ 'splice', |
@@ -88,3 +88,6 @@ /** | ||
p[s] = p[s].concat([value]); | ||
if (opts.persistent) | ||
p[s] = p[s].concat([value]); | ||
else | ||
p[s].push(value); | ||
} | ||
@@ -103,12 +106,15 @@ | ||
p[s] = [value].concat(p[s]); | ||
if (opts.persistent) | ||
p[s] = [value].concat(p[s]); | ||
else | ||
p[s].unshift(value); | ||
} | ||
/** | ||
* Append | ||
* Concat | ||
*/ | ||
else if (operationType === 'append') { | ||
else if (operationType === 'concat') { | ||
if (!type.array(p[s])) | ||
throw err( | ||
'append', | ||
'concat', | ||
'array', | ||
@@ -118,20 +124,9 @@ currentPath | ||
p[s] = p[s].concat(value); | ||
if (opts.persistent) | ||
p[s] = p[s].concat(value); | ||
else | ||
p[s].push.apply(p[s], value); | ||
} | ||
/** | ||
* Prepend | ||
*/ | ||
else if (operationType === 'prepend') { | ||
if (!type.array(p[s])) | ||
throw err( | ||
'prepend', | ||
'array', | ||
currentPath | ||
); | ||
p[s] = value.concat(p[s]); | ||
} | ||
/** | ||
* Splice | ||
@@ -147,3 +142,6 @@ */ | ||
p[s] = splice.apply(null, [p[s]].concat(value)); | ||
if (opts.persistent) | ||
p[s] = splice.apply(null, [p[s]].concat(value)); | ||
else | ||
p[s].splice.apply(p[s], value); | ||
} | ||
@@ -157,2 +155,5 @@ | ||
delete p[s]; | ||
else if (type.array(p)) | ||
p.splice(s, 1); | ||
} | ||
@@ -171,3 +172,6 @@ | ||
p[s] = shallowMerge({}, p[s], value); | ||
if (opts.persistent) | ||
p[s] = shallowMerge({}, p[s], value); | ||
else | ||
p[s] = shallowMerge(p[s], value); | ||
} | ||
@@ -174,0 +178,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
159657
4010
0
17
907
Updatedemmett@^3.1.1