Comparing version 0.1.0 to 0.2.0
{ | ||
"autoCommit": true, | ||
"asynchronous": true, | ||
"clone": false, | ||
"cloningFunction": null, | ||
"cursorSingletons": true, | ||
"delay": true, | ||
"maxHistory": 0, | ||
@@ -8,0 +8,0 @@ "typology": null, |
@@ -11,3 +11,3 @@ /** | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '0.1.0' | ||
value: '0.2.0' | ||
}); | ||
@@ -14,0 +14,0 @@ |
{ | ||
"name": "baobab", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "JavaScript data tree with cursors.", | ||
@@ -8,3 +8,3 @@ "main": "index.js", | ||
"emmett": "^2.1.1", | ||
"typology": "^0.2.1" | ||
"typology": "^0.3.0" | ||
}, | ||
@@ -15,2 +15,3 @@ "devDependencies": { | ||
"gulp": "^3.8.10", | ||
"gulp-header": "^1.2.2", | ||
"gulp-jshint": "^1.9.0", | ||
@@ -17,0 +18,0 @@ "gulp-mocha": "^2.0.0", |
156
README.md
@@ -7,6 +7,49 @@ [![Build Status](https://travis-ci.org/Yomguithereal/baobab.svg)](https://travis-ci.org/Yomguithereal/baobab) | ||
It is mainly inspired by functional zippers such as Clojure's [ones](http://clojuredocs.org/clojure.zip/zipper) 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. | ||
It can be paired with React easily through mixins to provide a centralized model holding your application's state. | ||
It can be paired with **React** easily through [mixins](#react-mixins) to provide a centralized model holding your application's state. | ||
## Summary | ||
* [Example](#example) | ||
* [Installation](#installation) | ||
* [Usage](#usage) | ||
* [Basics](#basics) | ||
* [Instantiation](#instantiation) | ||
* [Cursors](#cursors) | ||
* [Updates](#updates) | ||
* [Events](#events) | ||
* [React mixins](#react-mixins) | ||
* [Advanced](#advanced) | ||
* [Polymorphisms](#polymorphisms) | ||
* [Traversal](#traversal) | ||
* [Options](#options) | ||
* [History](#history) | ||
* [Update specifications](#update-specifications) | ||
* [Chaining mutations](#chaining-mutations) | ||
* [Data validation](#data-validation) | ||
* [Contribution](#contribution) | ||
* [License](#license) | ||
## Example | ||
```js | ||
var Baobab = require('baobab'); | ||
var tree = new Baobab({ | ||
palette: { | ||
colors: ['yellow', 'purple'], | ||
name: 'Glorious colors' | ||
} | ||
}); | ||
var colorsCursor = tree.select('palette', 'colors'); | ||
colorsCursor.on('update', function() { | ||
console.log('Selected colors have updated:', colorsCursor.get()); | ||
}); | ||
colorsCursor.push('orange'); | ||
``` | ||
## Installation | ||
@@ -31,17 +74,2 @@ | ||
* [Basics](#basics) | ||
* [Instantiation](#instantiation) | ||
* [Cursors](#cursors) | ||
* [Updates](#updates) | ||
* [Events](#events) | ||
* [React mixins](#react-mixins) | ||
* [Advanced](#advanced) | ||
* [Polymorphisms](#polymorphisms) | ||
* [Traversal](#traversal) | ||
* [Options](#options) | ||
* [History](#history) | ||
* [Update specifications](#update-specifications) | ||
* [Chaining mutations](#chaining-mutations) | ||
* [Data validation](#data-validation) | ||
### Basics | ||
@@ -116,3 +144,3 @@ | ||
```js | ||
cursor.set({hello: 'world'}); | ||
cursor.edit({hello: 'world'}); | ||
``` | ||
@@ -154,3 +182,3 @@ | ||
Whenever an update is committed, events are fired to notify relevant parts of the tree that data was changed so that bound element, 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, React components, for instance, can update. | ||
@@ -183,11 +211,5 @@ Note however that only relevant cursors will be notified of data change. | ||
// If we update the users | ||
usersCursor.update({ | ||
john: { | ||
firstname: {$set: 'John the third'} | ||
}, | ||
jack: { | ||
firstname: {$set: 'Jack the second'} | ||
} | ||
}); | ||
// If we update both users | ||
johnCursor.set('firstname', 'John the third'); | ||
jackCursor.set('firstname', 'Jack the second'); | ||
// Every cursor above will be notified of the update | ||
@@ -197,3 +219,3 @@ | ||
johnCursor.set('firstname', 'John the third'); | ||
// Only the users and john cursor will be notified | ||
// Only the users and john cursors will be notified | ||
``` | ||
@@ -213,3 +235,3 @@ | ||
Will fire if a data-validation specification was passed at instance and if new data does not abide by those specifications. | ||
Will fire if a data-validation specification was passed at instance and if new data does not abide by those specifications. For more information about this, see the [data validation](#data-validation) part of the documentation. | ||
@@ -254,3 +276,3 @@ ```js | ||
It is therefore really simple to bind this centralized model to React components by using the library's built-in mixins. Those will naturally bind components to one or more cursors watching over parts of the main state so they can update only when relevant data has been changed. | ||
It is then really simple to bind this centralized model to React components by using the library's built-in mixins. Those will naturally bind components to one or more cursors watching over parts of the main state so they can update only when relevant data has been changed. | ||
@@ -390,6 +412,8 @@ This basically makes the `shouldComponentUpdate` method useless in most of cases and ensures that your components will only re-render if they need to because of data changes. | ||
var tree = new Baobab({ | ||
list: [[1, 2], [3, 4]] | ||
list: [[1, 2], [3, 4]], | ||
longList: ['one', 'two', 'three', 'four'] | ||
}); | ||
var listCursor = tree.select('list'); | ||
var listCursor = tree.select('list'), | ||
twoCursor = tree.select('longList', 1); | ||
@@ -401,2 +425,8 @@ listCursor.down().right().get(); | ||
>>> 3 | ||
twoCursor.leftmost().get(); | ||
>>> 'one' | ||
twoCursor.rightmost().get(); | ||
>>> 'four' | ||
``` | ||
@@ -428,7 +458,7 @@ | ||
* **autoCommit** *boolean* [`true`]: should the tree auto commit updates or should it let the user do so through the `commit` method? | ||
* **clone** *boolean* [`false`]: by default, the tree will give access to references. Set to `true` to clone data when retrieving it from the tree. | ||
* **asynchronous** *boolean* [`true`]: should the tree delay the update to the next frame or fire them synchronously? | ||
* **clone** *boolean* [`false`]: by default, the tree will give access to references. Set to `true` to clone data when retrieving it from the tree if you feel paranoid and know you might mutate the references by accident or need a cloned object to handle. | ||
* **cloningFunction** *function*: the library's cloning method is minimalist on purpose and won't cover edgy cases. You remain free to pass your own more complex cloning function to the tree if needed. | ||
* **cursorSingletons** *boolean* [`true`]: by default, a *baobab* tree stashes the created cursor so only one would be created by path. You can override this behaviour by setting `cursorSingletons` to `false`. | ||
* **delay** *boolean* [`true`]: should the tree delay the update to next frame or fire them synchronously? | ||
* **maxHistory** *number* [`0`]: max number of records the tree is allowed to keep in its history. | ||
* **maxHistory** *number* [`0`]: max number of records the tree is allowed to store within its internal history. | ||
* **typology** *Typology|object*: a custom typology to be used to validate the tree's data. | ||
@@ -439,3 +469,3 @@ * **validate** *object*: a [typology](https://github.com/jacomyal/typology) schema ensuring the tree's data is valid. | ||
A *baobab* tree, given you pass it correct options, is able to record *n* of its passed states so you can go back in time whenever you want. | ||
A *baobab* tree, given you instantiate it with the correct option, is able to record *n* of its passed states so you can go back in time whenever you want. | ||
@@ -457,2 +487,13 @@ *Example* | ||
*Related Methods* | ||
```js | ||
// Check whether our tree hold records | ||
baobab.hasHistory(); | ||
>>> true | ||
// Retrieving history records | ||
baobab.getHistory(); | ||
``` | ||
#### Update specifications | ||
@@ -539,4 +580,43 @@ | ||
WIP | ||
Given you pass the correct parameters, a baobab tree is able to check whether its data is valid or not against the supplied specification. | ||
This specification must be written in the [typology](https://github.com/jacomyal/typology) library's style. | ||
*Example* | ||
```js | ||
var baobab = new Baobab( | ||
// Initial state | ||
{ | ||
hello: 'world', | ||
colors: ['yellow', 'blue'], | ||
counters: { | ||
users: 3, | ||
groups: 1 | ||
} | ||
}, | ||
// Parameters | ||
{ | ||
validate: { | ||
hello: '?string', | ||
colors: ['string'], | ||
counters: { | ||
users: 'number', | ||
groups: 'number' | ||
} | ||
} | ||
} | ||
); | ||
// If one updates the tree and does not respect the validation specification | ||
baobab.set('hello', 42); | ||
// Then the tree will fire an 'invalid' event containing a list of errors | ||
baobab.on('invalid', function(e) { | ||
console.log(e.data.errors); | ||
}); | ||
``` | ||
## Contribution | ||
@@ -543,0 +623,0 @@ |
@@ -5,3 +5,3 @@ /** | ||
* | ||
* Encloses an immutable set of data exposing useful cursors to its user. | ||
* A handy data tree with cursors. | ||
*/ | ||
@@ -37,5 +37,2 @@ var Cursor = require('./cursor.js'), | ||
// Properties | ||
this.data = this._cloner(initialData); | ||
// Privates | ||
@@ -57,5 +54,14 @@ this._futureUpdate = {}; | ||
if (!this.check()) | ||
throw Error('Baobab: instantiating with invalid data'); | ||
if (this.validate) | ||
try { | ||
this.typology.check(initialData, this.validate, true); | ||
} | ||
catch (e) { | ||
e.message = '/' + e.path.join('/') + ': ' + e.message; | ||
throw e; | ||
} | ||
// Properties | ||
this.data = this._cloner(initialData); | ||
// Mixin | ||
@@ -83,3 +89,3 @@ this.mixin = mixins.baobab(this); | ||
// Should we update synchronously? | ||
if (!this.options.delay) | ||
if (!this.options.asynchronous) | ||
return this.commit(); | ||
@@ -118,8 +124,2 @@ | ||
*/ | ||
Baobab.prototype.check = function() { | ||
return this.validate ? | ||
this.typology.check(this.data, this.validate) : | ||
true; | ||
}; | ||
Baobab.prototype.commit = function(referenceRecord) { | ||
@@ -145,5 +145,27 @@ var self = this, | ||
if (!this.check()) | ||
this.emit('invalid'); | ||
if (this.validate) { | ||
var errors = [], | ||
l = log.length, | ||
d, | ||
i; | ||
for (i = 0; i < l; i++) { | ||
d = helpers.getIn(this.validate, log[i]); | ||
if (!d) | ||
continue; | ||
try { | ||
this.typology.check(this.get(log[i]), d, true); | ||
} | ||
catch (e) { | ||
e.path = log[i].concat((e.path || [])); | ||
errors.push(e); | ||
} | ||
} | ||
if (errors.length) | ||
this.emit('invalid', {errors: errors}); | ||
} | ||
// Baobab-level update event | ||
@@ -150,0 +172,0 @@ this.emit('update', { |
@@ -122,2 +122,11 @@ /** | ||
Cursor.prototype.leftmost = function() { | ||
var last = +this.path[this.path.length - 1]; | ||
if (isNaN(last)) | ||
throw Error('baobab.Cursor.leftmost: cannot go left on a non-list type.'); | ||
return this.root.select(this.path.slice(0, -1).concat(0)); | ||
}; | ||
Cursor.prototype.right = function() { | ||
@@ -132,2 +141,13 @@ var last = +this.path[this.path.length - 1]; | ||
Cursor.prototype.rightmost = function() { | ||
var last = +this.path[this.path.length - 1]; | ||
if (isNaN(last)) | ||
throw Error('baobab.Cursor.right: cannot go right on a non-list type.'); | ||
var list = this.up().reference(); | ||
return this.root.select(this.path.slice(0, -1).concat(list.length - 1)); | ||
}; | ||
Cursor.prototype.down = function() { | ||
@@ -173,12 +193,14 @@ var last = +this.path[this.path.length - 1]; | ||
Cursor.prototype.set = function(key, value) { | ||
if (arguments.length < 2) { | ||
return this.update({$set: key}); | ||
} | ||
else { | ||
var spec = {}; | ||
spec[key] = {$set: value}; | ||
return this.update(spec); | ||
} | ||
if (arguments.length < 2) | ||
throw Error('baobab.Cursor.set: expecting at least key/value.'); | ||
var spec = {}; | ||
spec[key] = {$set: value}; | ||
return this.update(spec); | ||
}; | ||
Cursor.prototype.edit = function(value) { | ||
return this.update({$set: value}); | ||
}; | ||
Cursor.prototype.apply = function(fn) { | ||
@@ -199,3 +221,7 @@ if (typeof fn !== 'function') | ||
// TODO: consider dropping the ahead testing | ||
Cursor.prototype.push = function(value) { | ||
if (!(this.reference() instanceof Array)) | ||
throw Error('baobab.Cursor.push: trying to push to non-array value.'); | ||
if (arguments.length > 1) | ||
@@ -208,2 +234,5 @@ return this.update({$push: helpers.arrayOf(arguments)}); | ||
Cursor.prototype.unshift = function(value) { | ||
if (!(this.reference() instanceof Array)) | ||
throw Error('baobab.Cursor.push: trying to push to non-array value.'); | ||
if (arguments.length > 1) | ||
@@ -210,0 +239,0 @@ return this.update({$unshift: helpers.arrayOf(arguments)}); |
@@ -22,4 +22,4 @@ /** | ||
function makeError(path, message) { | ||
var e = new Error('precursors.update: ' + message + ' at path "/' + | ||
path.toString() + '"'); | ||
var e = new Error('precursors.update: ' + message + ' at path /' + | ||
path.toString()); | ||
@@ -26,0 +26,0 @@ e.path = path; |
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
40614
868
630
11
12
+ Addedtypology@0.3.1(transitive)
- Removedtypology@0.2.1(transitive)
Updatedtypology@^0.3.0