Comparing version 0.2.2 to 0.3.0
{ | ||
"name": "baobab", | ||
"main": "build/baobab.min.js", | ||
"version": "0.2.2", | ||
"version": "0.3.0", | ||
"homepage": "https://github.com/Yomguithereal/baobab", | ||
@@ -6,0 +6,0 @@ "author": { |
@@ -9,4 +9,5 @@ { | ||
"mixins": [], | ||
"shiftReferences": true, | ||
"typology": null, | ||
"validate": null | ||
} |
@@ -7,10 +7,14 @@ /** | ||
*/ | ||
var Baobab = require('./src/baobab.js'); | ||
var Baobab = require('./src/baobab.js'), | ||
helpers = require('./src/helpers.js'); | ||
// Non-writable version | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '0.2.2' | ||
value: '0.3.0' | ||
}); | ||
// Exposing helpers | ||
Baobab.getIn = helpers.getIn; | ||
// Exporting | ||
module.exports = Baobab; |
The MIT License (MIT) | ||
Copyright (c) 2014 Guillaume Plique (Yomguithereal) | ||
Copyright (c) 2014-2015 Guillaume Plique (Yomguithereal) | ||
@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy |
{ | ||
"name": "baobab", | ||
"version": "0.2.2", | ||
"version": "0.3.0", | ||
"description": "JavaScript data tree with cursors.", | ||
@@ -19,4 +19,4 @@ "main": "index.js", | ||
"gulp-uglify": "^1.0.2", | ||
"jsdom": "^2.0.0", | ||
"lodash.clonedeep": "^2.4.1", | ||
"jsdom": "^3.1.0", | ||
"lodash.clonedeep": "^3.0.0", | ||
"mocha": "^2.0.1", | ||
@@ -23,0 +23,0 @@ "react": "^0.12.2", |
149
README.md
@@ -29,3 +29,5 @@ [![Build Status](https://travis-ci.org/Yomguithereal/baobab.svg)](https://travis-ci.org/Yomguithereal/baobab) | ||
* [Chaining mutations](#chaining-mutations) | ||
* [Cursor combinations](#cursor-combinations) | ||
* [Data validation](#data-validation) | ||
* [Common pitfalls](#common-pitfalls) | ||
* [Contribution](#contribution) | ||
@@ -72,2 +74,8 @@ * [License](#license) | ||
Or install with bower: | ||
```js | ||
bower install baobab | ||
``` | ||
## Usage | ||
@@ -179,2 +187,8 @@ | ||
*Merging objects* | ||
```js | ||
cursor.merge({hello: 'world'}); | ||
``` | ||
#### Events | ||
@@ -297,2 +311,7 @@ | ||
// Cursor data is then available either through: | ||
var data = this.cursor.get(); | ||
// Or | ||
var data = this.state.cursor; | ||
return <ul>{this.cursor.get().map(renderItem)}</ul>; | ||
@@ -311,2 +330,7 @@ } | ||
// Cursor data is then available either through: | ||
var data = this.cursors[0].get(); | ||
// Or | ||
var data = this.state.cursors[0]; | ||
return ( | ||
@@ -333,2 +357,7 @@ <div> | ||
// Cursor data is then available either through: | ||
var data = this.cursors.name.get(); | ||
// Or | ||
var data = this.state.cursors.name; | ||
return ( | ||
@@ -374,3 +403,4 @@ <div> | ||
name: 'fancy', | ||
colors: ['blue', 'yellow', 'green'] | ||
colors: ['blue', 'yellow', 'green'], | ||
items: [{id: 'one', value: 'Hey'}, {id: 'two', value: 'Ho'}] | ||
} | ||
@@ -394,2 +424,17 @@ }); | ||
>>> ['blue', 'yellow', 'green'] | ||
// Retrieving or selecting data by passing a function in the path | ||
var complexCursor = tree.select('palette', 'colors', function(color) { | ||
return color === 'green'; | ||
}); | ||
tree.get('palette', 'colors', function(color) { | ||
return color === 'green'; | ||
}); | ||
>>> 'green' | ||
// Retrieving or selecting data by passing a descriptor object in the path | ||
var complexCursor = tree.select('items', {id: 'one'}, 'value'); | ||
tree.get('items', {id: 'one'}, 'value'); | ||
>>> 'Hey' | ||
``` | ||
@@ -432,2 +477,10 @@ | ||
*Check information about the cursor's location in the tree* | ||
```js | ||
cursor.isRoot(); | ||
cursor.isBranch(); | ||
cursor.isLeaf(); | ||
``` | ||
#### Options | ||
@@ -463,2 +516,3 @@ | ||
* **mixins** *array*: optional mixins to merge with baobab's ones. Recommending the [pure render](http://facebook.github.io/react/docs/pure-render-mixin.html) one from react. | ||
* **shiftReferences** *boolean* [`false`]: tell the tree to shift references of the objects it updates so that functions performing shallow comparisons (such as the one used by the `PureRenderMixin`, for instance), can assess that data changed. | ||
* **typology** *Typology|object*: a custom typology to be used to validate the tree's data. | ||
@@ -514,2 +568,3 @@ * **validate** *object*: a [typology](https://github.com/jacomyal/typology) schema ensuring the tree's data is valid. | ||
* `$unshift` | ||
* `$merge` | ||
@@ -534,12 +589,14 @@ *Example* | ||
tree.update({ | ||
john: { | ||
firstname: { | ||
$set: 'John the 3rd' | ||
} | ||
}, | ||
jack: { | ||
firstname: { | ||
$apply: function(firstname) { | ||
return firstname + ' the 2nd'; | ||
users: { | ||
john: { | ||
firstname: { | ||
$set: 'John the 3rd' | ||
} | ||
}, | ||
jack: { | ||
firstname: { | ||
$apply: function(firstname) { | ||
return firstname + ' the 2nd'; | ||
} | ||
} | ||
} | ||
@@ -550,3 +607,3 @@ } | ||
// From cursor | ||
var cursor = tree.select('john'); | ||
var cursor = tree.select('users', 'john'); | ||
cursor.update({ | ||
@@ -579,2 +636,25 @@ firstname: { | ||
#### Cursor combinations | ||
At times, you might want to listen to updates concerning a logical combination of cursors. For instance, you might want to know when two cursors both updated or when either one or the other did. | ||
You can build cursor combination likewise: | ||
```js | ||
// Simple "or" combination | ||
var combination = cursor1.or(cursor2); | ||
// Simple "and" combination | ||
var combination = cursor1.and(cursor2); | ||
// Complex combination | ||
var combination = cursor1.or(cursor2).or(cursor3).and(cursor4); | ||
// Listening to events | ||
combination.on('update', handler); | ||
// Releasing a combination to avoid leaks | ||
combination.release(); | ||
``` | ||
#### Data validation | ||
@@ -623,2 +703,49 @@ | ||
#### Common pitfalls | ||
*Controlled input state* | ||
If you need to store a react controlled input's state into a baobab tree, remember you have to commit changes synchronously through the `commit` method if you don't want to observe nasty cursor jumps. | ||
```jsx | ||
var tree = new Boabab({inputValue: null}); | ||
var Input = React.createClass({ | ||
mixins: [tree.mixin], | ||
cursor: ['inputValue'], | ||
onChange: function(e) { | ||
var newValue = e.target.value; | ||
// If one edits the tree normally, i.e. asynchronously, the cursor will hop | ||
this.cursor.edit(newValue); | ||
// One has to commit synchronously the update for the input to work correctly | ||
this.cursor.edit(newValue); | ||
this.tree.commit(); | ||
}, | ||
render: function() { | ||
return <input onChange={this.onChange} value={this.cursor.get()} />; | ||
} | ||
}); | ||
``` | ||
*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'; | ||
``` | ||
## Contribution | ||
@@ -625,0 +752,0 @@ |
@@ -33,4 +33,4 @@ /** | ||
// Merging defaults | ||
this.options = merge(opts, defaults); | ||
this._cloner = this.options.cloningFunction || helpers.clone; | ||
this.options = helpers.shallowMerge(defaults, opts); | ||
this._cloner = this.options.cloningFunction || helpers.deepClone; | ||
@@ -135,3 +135,3 @@ // Privates | ||
var record = this._archive(); | ||
log = update(this.data, this._futureUpdate); | ||
log = update(this.data, this._futureUpdate, this.options); | ||
@@ -187,4 +187,14 @@ if (record) | ||
// Casting to array | ||
path = (typeof path === 'string') ? [path] : path; | ||
path = (types.get(path) !== 'array') ? [path] : path; | ||
// Complex path? | ||
var complex = path.some(function(step) { | ||
return types.check(step, 'complexStep'); | ||
}); | ||
var solvedPath; | ||
if (complex) | ||
solvedPath = helpers.solvePath(this.data, path); | ||
// Registering a new cursor or giving the already existing one for path | ||
@@ -198,3 +208,3 @@ if (!this.options.cursorSingletons) { | ||
if (!this._registeredCursors[hash]) { | ||
var cursor = new Cursor(this, path); | ||
var cursor = new Cursor(this, path, solvedPath); | ||
this._registeredCursors[hash] = cursor; | ||
@@ -275,3 +285,3 @@ return cursor; | ||
Baobab.prototype.toJSON = function() { | ||
return this.get(); | ||
return this.reference(); | ||
}; | ||
@@ -278,0 +288,0 @@ |
@@ -8,2 +8,3 @@ /** | ||
var EventEmitter = require('emmett'), | ||
Combination = require('./combination.js'), | ||
mixins = require('./mixins.js'), | ||
@@ -16,3 +17,3 @@ helpers = require('./helpers.js'), | ||
*/ | ||
function Cursor(root, path) { | ||
function Cursor(root, path, solvedPath) { | ||
var self = this; | ||
@@ -31,2 +32,6 @@ | ||
// Complex path? | ||
this.complexPath = !!solvedPath; | ||
this.solvedPath = this.complexPath ? solvedPath : this.path; | ||
// Root listeners | ||
@@ -38,2 +43,10 @@ this.root.on('update', function(e) { | ||
// Solving path if needed | ||
if (self.complexPath) | ||
self.solvedPath = helpers.solvePath(self.root.data, self.path); | ||
// If no handlers are attached, we stop | ||
if (!this._handlers.update.length && !this._handlersAll.length) | ||
return; | ||
// If selector listens at root, we fire | ||
@@ -52,7 +65,7 @@ if (!self.path.length) | ||
// If path is not relevant to us, we break | ||
if (p !== self.path[j]) | ||
if (p !== self.solvedPath[j]) | ||
break; | ||
// If we reached last item and we are relevant, we fire | ||
if (j + 1 === m || j + 1 === self.path.length) { | ||
if (j + 1 === m || j + 1 === self.solvedPath.length) { | ||
shouldFire = true; | ||
@@ -95,3 +108,3 @@ break root; | ||
Cursor.prototype._stack = function(spec) { | ||
this.root._stack(helpers.pathObject(this.path, spec)); | ||
this.root._stack(helpers.pathObject(this.solvedPath, spec)); | ||
return this; | ||
@@ -101,4 +114,19 @@ }; | ||
/** | ||
* Prototype | ||
* Predicates | ||
*/ | ||
Cursor.prototype.isRoot = function() { | ||
return !this.path.length; | ||
}; | ||
Cursor.prototype.isLeaf = function() { | ||
return types.check(this.reference(), 'primitive'); | ||
}; | ||
Cursor.prototype.isBranch = function() { | ||
return !this.isLeaf() && !this.isRoot(); | ||
}; | ||
/** | ||
* Traversal | ||
*/ | ||
Cursor.prototype.select = function(path) { | ||
@@ -114,10 +142,10 @@ if (arguments.length > 1) | ||
Cursor.prototype.up = function() { | ||
if (this.path.length) | ||
if (this.solvedPath && this.solvedPath.length) | ||
return this.root.select(this.path.slice(0, -1)); | ||
else | ||
return this.root.select([]); | ||
return null; | ||
}; | ||
Cursor.prototype.left = function() { | ||
var last = +this.path[this.path.length - 1]; | ||
var last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -127,7 +155,9 @@ if (isNaN(last)) | ||
return this.root.select(this.path.slice(0, -1).concat(last - 1)); | ||
return last ? | ||
this.root.select(this.solvedPath.slice(0, -1).concat(last - 1)) : | ||
null; | ||
}; | ||
Cursor.prototype.leftmost = function() { | ||
var last = +this.path[this.path.length - 1]; | ||
var last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -137,7 +167,7 @@ if (isNaN(last)) | ||
return this.root.select(this.path.slice(0, -1).concat(0)); | ||
return this.root.select(this.solvedPath.slice(0, -1).concat(0)); | ||
}; | ||
Cursor.prototype.right = function() { | ||
var last = +this.path[this.path.length - 1]; | ||
var last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -147,7 +177,10 @@ if (isNaN(last)) | ||
return this.root.select(this.path.slice(0, -1).concat(last + 1)); | ||
if (last + 1 === this.up().reference().length) | ||
return null; | ||
return this.root.select(this.solvedPath.slice(0, -1).concat(last + 1)); | ||
}; | ||
Cursor.prototype.rightmost = function() { | ||
var last = +this.path[this.path.length - 1]; | ||
var last = +this.solvedPath[this.solvedPath.length - 1]; | ||
@@ -159,14 +192,17 @@ if (isNaN(last)) | ||
return this.root.select(this.path.slice(0, -1).concat(list.length - 1)); | ||
return this.root.select(this.solvedPath.slice(0, -1).concat(list.length - 1)); | ||
}; | ||
Cursor.prototype.down = function() { | ||
var last = +this.path[this.path.length - 1]; | ||
var last = +this.solvedPath[this.solvedPath.length - 1]; | ||
if (!(this.reference() instanceof Array)) | ||
throw Error('baobab.Cursor.down: cannot descend on a non-list type.'); | ||
return null; | ||
return this.root.select(this.path.concat(0)); | ||
return this.root.select(this.solvedPath.concat(0)); | ||
}; | ||
/** | ||
* Access | ||
*/ | ||
Cursor.prototype.get = function(path) { | ||
@@ -176,6 +212,6 @@ if (arguments.length > 1) | ||
if (types.check(path, 'string|number|array')) | ||
return this.root.get(this.path.concat(path)); | ||
if (types.check(path, 'step')) | ||
return this.root.get(this.solvedPath.concat(path)); | ||
else | ||
return this.root.get(this.path); | ||
return this.root.get(this.solvedPath); | ||
}; | ||
@@ -187,6 +223,6 @@ | ||
if (types.check(path, 'string|number|array')) | ||
return this.root.reference(this.path.concat(path)); | ||
if (types.check(path, 'step')) | ||
return this.root.reference(this.solvedPath.concat(path)); | ||
else | ||
return this.root.reference(this.path); | ||
return this.root.reference(this.solvedPath); | ||
}; | ||
@@ -198,8 +234,11 @@ | ||
if (types.check(path, 'string|number|array')) | ||
return this.root.clone(this.path.concat(path)); | ||
if (types.check(path, 'step')) | ||
return this.root.clone(this.solvedPath.concat(path)); | ||
else | ||
return this.root.clone(this.path); | ||
return this.root.clone(this.solvedPath); | ||
}; | ||
/** | ||
* Update | ||
*/ | ||
Cursor.prototype.set = function(key, value) { | ||
@@ -254,2 +293,12 @@ if (arguments.length < 2) | ||
Cursor.prototype.merge = function(o) { | ||
if (!types.check(o, 'object')) | ||
throw Error('baobab.Cursor.merge: trying to merge a non-object.'); | ||
if (!types.check(this.reference(), 'object')) | ||
throw Error('baobab.Cursor.merge: trying to merge into a non-object.'); | ||
this.update({$merge: o}); | ||
}; | ||
Cursor.prototype.update = function(spec) { | ||
@@ -260,2 +309,13 @@ return this._stack(spec); | ||
/** | ||
* Combination | ||
*/ | ||
Cursor.prototype.or = function(otherCursor) { | ||
return new Combination('or', this, otherCursor); | ||
}; | ||
Cursor.prototype.and = function(otherCursor) { | ||
return new Combination('and', this, otherCursor); | ||
}; | ||
/** | ||
* Type definition | ||
@@ -271,3 +331,3 @@ */ | ||
Cursor.prototype.toJSON = function() { | ||
return this.get(); | ||
return this.reference(); | ||
}; | ||
@@ -274,0 +334,0 @@ |
@@ -14,34 +14,63 @@ /** | ||
// Deep clone an object | ||
function clone(item) { | ||
if (!item) | ||
// Shallow merge | ||
function shallowMerge(o1, o2) { | ||
var o = {}, | ||
k; | ||
for (k in o1) o[k] = o1[k]; | ||
for (k in o2) o[k] = o2[k]; | ||
return o; | ||
} | ||
// Shallow clone | ||
function shallowClone(item) { | ||
if (!item || !(item instanceof Object)) | ||
return item; | ||
var result, | ||
i, | ||
k, | ||
l; | ||
// Array | ||
if (types.get(item) === 'array') | ||
return item.slice(0); | ||
if (types.check(item, 'array')) { | ||
result = []; | ||
// Date | ||
if (types.get(item) === 'date') | ||
return new Date(item.getTime()); | ||
// Object | ||
if (types.get(item) === 'object') { | ||
var k, o = {}; | ||
for (k in item) | ||
o[k] = item[k]; | ||
return o; | ||
} | ||
return item; | ||
} | ||
// Deep clone | ||
function deepClone(item) { | ||
if (!item || !(item instanceof Object)) | ||
return item; | ||
// Array | ||
if (types.get(item) === 'array') { | ||
var i, l, a = []; | ||
for (i = 0, l = item.length; i < l; i++) | ||
result.push(clone(item[i])); | ||
a.push(deepClone(item[i])); | ||
return a; | ||
} | ||
} else if (types.check(item, 'date')) { | ||
result = new Date(item.getTime()); | ||
// Date | ||
if (types.get(item) === 'date') | ||
return new Date(item.getTime()); | ||
} else if (types.check(item, 'object')) { | ||
if (item.nodeType && typeof item.cloneNode === 'function') | ||
result = item; | ||
else if (!item.prototype) { | ||
result = {}; | ||
for (i in item) | ||
result[i] = clone(item[i]); | ||
} else | ||
result = item; | ||
} else { | ||
result = item; | ||
// Object | ||
if (types.get(item) === 'object') { | ||
var k, o = {}; | ||
for (k in item) | ||
o[k] = deepClone(item[k]); | ||
return o; | ||
} | ||
return result; | ||
return item; | ||
} | ||
@@ -56,2 +85,54 @@ | ||
// Get first item matching predicate in list | ||
function first(a, fn) { | ||
var i, l; | ||
for (i = 0, l = a.length; i < l; i++) { | ||
if (fn(a[i])) | ||
return a[i]; | ||
} | ||
return; | ||
} | ||
function index(a, fn) { | ||
var i, l; | ||
for (i = 0, l = a.length; i < l; i++) { | ||
if (fn(a[i])) | ||
return i; | ||
} | ||
return -1; | ||
} | ||
// Compare object to spec | ||
function compare(object, spec) { | ||
var ok = true, | ||
k; | ||
for (k in spec) { | ||
if (types.get(spec[k]) === 'object') { | ||
ok = ok && compare(object[k]); | ||
} | ||
else if (types.get(spec[k]) === 'array') { | ||
ok = ok && !!~spec[k].indexOf(object[k]); | ||
} | ||
else { | ||
if (object[k] !== spec[k]) | ||
return false; | ||
} | ||
} | ||
return ok; | ||
} | ||
function firstByComparison(object, spec) { | ||
return first(object, function(e) { | ||
return compare(e, spec); | ||
}); | ||
} | ||
function indexByComparison(object, spec) { | ||
return indexOf(object, function(e) { | ||
return compare(e, spec); | ||
}); | ||
} | ||
// Retrieve nested objects | ||
@@ -68,3 +149,18 @@ function getIn(object, path) { | ||
return; | ||
c = c[path[i]]; | ||
if (typeof path[i] === 'function') { | ||
if (types.get(c) !== 'array') | ||
return; | ||
c = first(c, path[i]); | ||
} | ||
else if (typeof path[i] === 'object') { | ||
if (types.get(c) !== 'array') | ||
return; | ||
c = firstByComparison(c, path[i]); | ||
} | ||
else { | ||
c = c[path[i]]; | ||
} | ||
} | ||
@@ -75,2 +171,39 @@ | ||
// Solve a complex path | ||
function solvePath(object, path) { | ||
var 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 (types.get(c) !== 'array') | ||
return; | ||
idx = index(c, path[i]); | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} | ||
else if (typeof path[i] === 'object') { | ||
if (types.get(c) !== 'array') | ||
return; | ||
idx = index(indexByComparison(c, path[i])); | ||
solvedPath.push(idx); | ||
c = c[idx]; | ||
} | ||
else { | ||
solvedPath.push(path[i]); | ||
c = c[path[i]]; | ||
} | ||
} | ||
return solvedPath; | ||
} | ||
// Return a fake object relative to the given path | ||
@@ -111,3 +244,5 @@ function pathObject(path, spec) { | ||
arrayOf: arrayOf, | ||
clone: clone, | ||
deepClone: deepClone, | ||
shallowClone: shallowClone, | ||
shallowMerge: shallowMerge, | ||
compose: compose, | ||
@@ -117,3 +252,4 @@ getIn: getIn, | ||
later: later, | ||
pathObject: pathObject | ||
pathObject: pathObject, | ||
solvePath: solvePath | ||
}; |
@@ -43,26 +43,34 @@ /** | ||
// Upper $set/$apply and conflicts | ||
// TODO: Boooo! Ugly... | ||
if (hasOneOf(arguments[i], ['$set', '$apply', '$chain'])) { | ||
if (res.$set && (arguments[i].$apply || arguments[i].$chain)) { | ||
delete res.$set; | ||
res.$apply = arguments[i].$apply || arguments[i].$chain; | ||
} | ||
else if (res.$apply && arguments[i].$set) { | ||
delete res.$apply; | ||
res.$set = arguments[i].$set; | ||
} | ||
else if (arguments[i].$set) { | ||
res.$set = arguments[i].$set; | ||
} | ||
else if (arguments[i].$apply) { | ||
res.$apply = arguments[i].$apply; | ||
} | ||
else if (arguments[i].$chain) { | ||
if (res.$apply) | ||
res.$apply = helpers.compose(res.$apply, arguments[i].$chain); | ||
else | ||
res.$apply = arguments[i].$chain; | ||
} | ||
// Upper $set/$apply... and conflicts | ||
// When solving conflicts, here is the priority to apply: | ||
// -- 1) $set | ||
// -- 2) $merge | ||
// -- 3) $apply | ||
// -- 4) $chain | ||
if (arguments[i].$set) { | ||
delete res.$apply; | ||
delete res.$merge; | ||
res.$set = arguments[i].$set; | ||
continue; | ||
} | ||
else if (arguments[i].$merge) { | ||
delete res.$set; | ||
delete res.$apply; | ||
res.$merge = arguments[i].$merge; | ||
continue; | ||
} | ||
else if (arguments[i].$apply){ | ||
delete res.$set; | ||
delete res.$merge; | ||
res.$apply = arguments[i].$apply; | ||
continue; | ||
} | ||
else if (arguments[i].$chain) { | ||
delete res.$set; | ||
delete res.$merge; | ||
if (res.$apply) | ||
res.$apply = helpers.compose(res.$apply, arguments[i].$chain); | ||
else | ||
res.$apply = arguments[i].$chain; | ||
continue; | ||
@@ -69,0 +77,0 @@ } |
@@ -7,3 +7,4 @@ /** | ||
*/ | ||
var types = require('./typology.js'); | ||
var types = require('./typology.js'), | ||
Combination = require('./combination.js'); | ||
@@ -13,83 +14,95 @@ module.exports = { | ||
return { | ||
mixins: baobab.options.mixins, | ||
componentWillMount: function() { | ||
// Binding baobab to instance | ||
this.baobab = baobab; | ||
this.__type = null; | ||
// Run Baobab mixin first to allow mixins to access cursors | ||
mixins: [{ | ||
getInitialState: function() { | ||
// Is there any cursors to create? | ||
if (this.cursor && this.cursors) | ||
throw Error('baobab.mixin: you cannot have both ' + | ||
'`component.cursor` and `component.cursors`. Please ' + | ||
'make up your mind.'); | ||
// Binding baobab to instance | ||
this.tree = baobab; | ||
this.__type = null; | ||
if (this.cursor) { | ||
if (!types.check(this.cursor, 'string|number|array|cursor')) | ||
throw Error('baobab.mixin.cursor: invalid data (cursor, string or array).'); | ||
// Is there any cursors to create? | ||
if (this.cursor && this.cursors) | ||
throw Error('baobab.mixin: you cannot have both ' + | ||
'`component.cursor` and `component.cursors`. Please ' + | ||
'make up your mind.'); | ||
if (!types.check(this.cursor, 'cursor')) | ||
this.cursor = baobab.select(this.cursor); | ||
this.__type = 'single'; | ||
} | ||
else if (this.cursors) { | ||
if (!types.check(this.cursors, 'object|array')) | ||
throw Error('baobab.mixin.cursor: invalid data (object or array).'); | ||
// Making update handler | ||
this.__updateHandler = (function() { | ||
this.setState(this.__getCursorData()); | ||
}).bind(this); | ||
if (types.check(this.cursors, 'array')) { | ||
this.cursors = this.cursors.map(function(path) { | ||
return types.check(path, 'cursor') ? path : baobab.select(path); | ||
}); | ||
this.__type = 'array'; | ||
if (this.cursor) { | ||
if (!types.check(this.cursor, 'string|number|array|cursor')) | ||
throw Error('baobab.mixin.cursor: invalid data (cursor, string or array).'); | ||
if (!types.check(this.cursor, 'cursor')) | ||
this.cursor = baobab.select(this.cursor); | ||
this.__getCursorData = (function() { | ||
return {cursor: this.cursor.get()}; | ||
}).bind(this); | ||
this.__type = 'single'; | ||
} | ||
else { | ||
// TODO: better validation | ||
for (var k in this.cursors) { | ||
if (!types.check(this.cursors[k], 'cursor')) | ||
this.cursors[k] = baobab.select(this.cursors[k]); | ||
else if (this.cursors) { | ||
if (!types.check(this.cursors, 'object|array')) | ||
throw Error('baobab.mixin.cursor: invalid data (object or array).'); | ||
if (types.check(this.cursors, 'array')) { | ||
this.cursors = this.cursors.map(function(path) { | ||
return types.check(path, 'cursor') ? path : baobab.select(path); | ||
}); | ||
this.__getCursorData = (function() { | ||
return {cursors: this.cursors.map(function(cursor) { | ||
return cursor.get(); | ||
})}; | ||
}).bind(this); | ||
this.__type = 'array'; | ||
} | ||
this.__type = 'object'; | ||
else { | ||
for (var k in this.cursors) { | ||
if (!types.check(this.cursors[k], 'cursor')) | ||
this.cursors[k] = baobab.select(this.cursors[k]); | ||
} | ||
this.__getCursorData = (function() { | ||
var d = {}; | ||
for (k in this.cursors) | ||
d[k] = this.cursors[k].get(); | ||
return {cursors: d}; | ||
}).bind(this); | ||
this.__type = 'object'; | ||
} | ||
} | ||
} | ||
// Making update handler | ||
var fired = false; | ||
this.__updateHandler = (function() { | ||
if (!fired) { | ||
this.forceUpdate(); | ||
fired = true; | ||
setTimeout(function() { | ||
fired = false; | ||
}, 0); | ||
return this.__getCursorData(); | ||
}, | ||
componentDidMount: function() { | ||
if (this.__type === 'single') { | ||
this.cursor.on('update', this.__updateHandler); | ||
} | ||
}).bind(this); | ||
}, | ||
componentDidMount: function() { | ||
if (this.__type === 'single') { | ||
this.cursor.on('update', this.__updateHandler); | ||
else if (this.__type === 'array') { | ||
this.__combination = new Combination('or', this.cursors); | ||
this.__combination.on('update', this.__updateHandler); | ||
} | ||
else if (this.__type === 'object') { | ||
this.__combination = new Combination( | ||
'or', | ||
Object.keys(this.cursors).map(function(k) { | ||
return this.cursors[k]; | ||
}, this) | ||
); | ||
this.__combination.on('update', this.__updateHandler); | ||
} | ||
}, | ||
componentWillUnmount: function() { | ||
if (this.__type === 'single') { | ||
this.cursor.off('update', this.__updateHandler); | ||
} | ||
else { | ||
this.__combination.release(); | ||
} | ||
} | ||
else if (this.__type === 'array') { | ||
this.cursors.forEach(function(cursor) { | ||
cursor.on('update', this.__updateHandler); | ||
}, this); | ||
} | ||
else if (this.__type === 'object') { | ||
for (var k in this.cursors) | ||
this.cursors[k].on('update', this.__updateHandler); | ||
} | ||
}, | ||
componentWillUnmount: function() { | ||
if (this.__type === 'single') { | ||
this.cursor.off('update', this.__updateHandler); | ||
} | ||
else if (this.__type === 'array') { | ||
this.cursors.forEach(function(cursor) { | ||
cursor.off('update', this.__updateHandler); | ||
}, this); | ||
} | ||
else if (this.__type === 'object') { | ||
for (var k in this.cursors) | ||
this.cursors[k].off('update', this.__updateHandler); | ||
} | ||
} | ||
}].concat(baobab.options.mixins) | ||
}; | ||
@@ -99,25 +112,30 @@ }, | ||
return { | ||
mixins: cursor.root.options.mixins, | ||
componentWillMount: function() { | ||
// Binding cursor to instance | ||
this.cursor = cursor; | ||
// Run cursor mixin first to allow mixins to access cursors | ||
mixins: [{ | ||
getInitialState: function() { | ||
// Making update handler | ||
this.__updateHandler = (function() { | ||
this.forceUpdate(); | ||
}).bind(this); | ||
}, | ||
componentDidMount: function() { | ||
// Binding cursor to instance | ||
this.cursor = cursor; | ||
// Listening to updates | ||
this.cursor.on('update', this.__updateHandler); | ||
}, | ||
componentWillUnmount: function() { | ||
// Making update handler | ||
this.__updateHandler = (function() { | ||
this.setState({cursor: this.cursor.get()}); | ||
}).bind(this); | ||
// Unbinding handler | ||
this.cursor.off('update', this.__updateHandler); | ||
} | ||
return {cursor: this.cursor.get()}; | ||
}, | ||
componentDidMount: function() { | ||
// Listening to updates | ||
this.cursor.on('update', this.__updateHandler); | ||
}, | ||
componentWillUnmount: function() { | ||
// Unbinding handler | ||
this.cursor.off('update', this.__updateHandler); | ||
} | ||
}].concat(cursor.root.options.mixins) | ||
}; | ||
} | ||
}; |
@@ -10,4 +10,7 @@ /** | ||
var typology = new Typology({ | ||
complexStep: 'function|object', | ||
step: 'string|number|array|function|object', | ||
path: function(v) { | ||
return this.check(v, '?string|number') || this.check(v, ['string|number']); | ||
return this.check(v, '?string|number|function|object') || | ||
this.check(v, ['string|number|function|object']); | ||
}, | ||
@@ -14,0 +17,0 @@ typology: function(v) { |
@@ -8,3 +8,4 @@ /** | ||
*/ | ||
var types = require('./typology.js'); | ||
var types = require('./typology.js'), | ||
helpers = require('./helpers.js'); | ||
@@ -16,3 +17,4 @@ var COMMANDS = {}; | ||
'$unshift', | ||
'$apply' | ||
'$apply', | ||
'$merge' | ||
].forEach(function(c) { | ||
@@ -31,91 +33,117 @@ COMMANDS[c] = true; | ||
// Function mutating the object for performance reasons | ||
function mutator(log, o, spec, path) { | ||
path = path || []; | ||
// Core function | ||
function update(target, spec, opts) { | ||
opts = opts || {}; | ||
var log = {}; | ||
var hash = path.join('λ'), | ||
fn, | ||
h, | ||
k, | ||
v, | ||
i, | ||
l; | ||
// Closure mutating the internal object | ||
(function mutator(o, spec, path) { | ||
path = path || []; | ||
for (k in spec) { | ||
if (COMMANDS[k]) { | ||
v = spec[k]; | ||
var hash = path.join('λ'), | ||
fn, | ||
h, | ||
k, | ||
v, | ||
i, | ||
l; | ||
// Logging update | ||
if (hash && !log[hash]) | ||
for (k in spec) { | ||
if (COMMANDS[k]) { | ||
v = spec[k]; | ||
// Logging update | ||
log[hash] = true; | ||
// Applying | ||
switch (k) { | ||
case '$push': | ||
if (!types.check(o, 'array')) | ||
throw makeError(path, 'using command $push to a non array'); | ||
// Applying | ||
switch (k) { | ||
case '$push': | ||
if (!types.check(o, 'array')) | ||
throw makeError(path, 'using command $push to a non array'); | ||
if (!types.check(v, 'array')) | ||
o.push(v); | ||
else | ||
o.push.apply(o, v); | ||
break; | ||
case '$unshift': | ||
if (!types.check(o, 'array')) | ||
throw makeError(path, 'using command $unshift to a non array'); | ||
if (!types.check(v, 'array')) | ||
o.push(v); | ||
else | ||
o.push.apply(o, v); | ||
break; | ||
case '$unshift': | ||
if (!types.check(o, 'array')) | ||
throw makeError(path, 'using command $unshift to a non array'); | ||
if (!types.check(v, 'array')) | ||
o.unshift(v); | ||
else | ||
o.unshift.apply(o, v); | ||
break; | ||
if (!types.check(v, 'array')) | ||
o.unshift(v); | ||
else | ||
o.unshift.apply(o, v); | ||
break; | ||
} | ||
} | ||
} | ||
else { | ||
if ('$set' in (spec[k] || {})) { | ||
else { | ||
h = hash ? hash + 'λ' + k : k; | ||
v = spec[k].$set; | ||
// Logging update | ||
if (h && !log[h]) | ||
if ('$set' in (spec[k] || {})) { | ||
v = spec[k].$set; | ||
// Logging update | ||
log[h] = true; | ||
o[k] = v; | ||
} | ||
else if ('$apply' in (spec[k] || {})) { | ||
h = hash ? hash + 'λ' + k : k; | ||
fn = spec[k].$apply; | ||
o[k] = v; | ||
} | ||
else if ('$apply' in (spec[k] || {})) { | ||
fn = spec[k].$apply; | ||
if (typeof fn !== 'function') | ||
throw makeError(path.concat(k), 'using command $apply with a non function'); | ||
if (typeof fn !== 'function') | ||
throw makeError(path.concat(k), 'using command $apply with a non function'); | ||
// Logging update | ||
if (h && !log[h]) | ||
// Logging update | ||
log[h] = true; | ||
o[k] = fn.call(null, o[k]); | ||
} | ||
else { | ||
o[k] = fn.call(null, o[k]); | ||
} | ||
else if ('$merge' in (spec[k] || {})) { | ||
v = spec[k].$merge; | ||
// If nested object does not exist, we create it | ||
if (typeof o[k] === 'undefined') | ||
o[k] = {}; | ||
if (!types.check(o[k], 'object')) | ||
throw makeError(path.concat(k), 'using command $merge on a non-object'); | ||
// Recur | ||
mutator( | ||
log, | ||
o[k], | ||
spec[k], | ||
path.concat(k) | ||
); | ||
// Logging update | ||
log[h] = true; | ||
o[k] = helpers.shallowMerge(o[k], v); | ||
} | ||
else if (opts.shiftReferences && | ||
('$push' in (spec[k] || {}) || | ||
'$unshift' in (spec[k] || {}))) { | ||
if ('$push' in (spec[k] || {})) { | ||
v = spec[k].$push; | ||
if (!types.check(o[k], 'array')) | ||
throw makeError(path.concat(k), 'using command $push to a non array'); | ||
o[k] = o[k].concat(v); | ||
} | ||
if ('$unshift' in (spec[k] || {})) { | ||
v = spec[k].$unshift; | ||
if (!types.check(o[k], 'array')) | ||
throw makeError(path.concat(k), 'using command $unshift to a non array'); | ||
o[k] = (v instanceof Array ? v : [v]).concat(o[k]); | ||
} | ||
// Logging update | ||
log[h] = true; | ||
} | ||
else { | ||
// If nested object does not exist, we create it | ||
if (typeof o[k] === 'undefined') | ||
o[k] = {}; | ||
// Recur | ||
mutator( | ||
o[k], | ||
spec[k], | ||
path.concat(k) | ||
); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
})(target, spec); | ||
// Core function | ||
function update(target, spec) { | ||
var log = {}; | ||
mutator(log, target, spec); | ||
return Object.keys(log).map(function(hash) { | ||
@@ -122,0 +150,0 @@ return hash.split('λ'); |
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
54442
14
1216
758