Comparing version 0.0.1 to 0.1.0
{ | ||
"autoCommit": true, | ||
"clone": false, | ||
"cloningFunction": null, | ||
"cursorSingletons": true, | ||
"delay": true, | ||
"toJS": false, | ||
"maxHistory": 0 | ||
"maxHistory": 0, | ||
"typology": null, | ||
"validate": null | ||
} |
var gulp = require('gulp'), | ||
jshint = require('gulp-jshint'), | ||
mocha = require('gulp-mocha'); | ||
mocha = require('gulp-mocha'), | ||
uglify = require('gulp-uglify'), | ||
rename = require('gulp-rename'), | ||
transform = require('vinyl-transform'), | ||
browserify = require('browserify'); | ||
@@ -21,3 +25,16 @@ // Files | ||
// Building | ||
gulp.task('build', function() { | ||
var bundle = transform(function(filename) { | ||
return browserify({entries: filename, standalone: 'Baobab'}).bundle(); | ||
}); | ||
return gulp.src('./index.js') | ||
.pipe(bundle) | ||
.pipe(uglify()) | ||
.pipe(rename('baobab.min.js')) | ||
.pipe(gulp.dest('./build')); | ||
}); | ||
// Default | ||
gulp.task('default', ['lint', 'test']); | ||
gulp.task('default', ['lint', 'test', 'build']); |
@@ -11,3 +11,3 @@ /** | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '0.0.1' | ||
value: '0.1.0' | ||
}); | ||
@@ -14,0 +14,0 @@ |
{ | ||
"name": "baobab", | ||
"version": "0.0.1", | ||
"description": "JavaScript cursors for immutable data structures.", | ||
"version": "0.1.0", | ||
"description": "JavaScript data tree with cursors.", | ||
"main": "index.js", | ||
"dependencies": { | ||
"emmett": "^2.1.1", | ||
"immutable": "^3.3.0", | ||
"typology": "^0.2.1" | ||
@@ -13,9 +12,15 @@ }, | ||
"async": "~0.9.0", | ||
"mocha": "^2.0.1", | ||
"browserify": "^7.0.0", | ||
"gulp": "^3.8.10", | ||
"gulp-jshint": "^1.9.0", | ||
"gulp-mocha": "^2.0.0", | ||
"gulp": "^3.8.10" | ||
"gulp-rename": "^1.2.0", | ||
"gulp-uglify": "^1.0.2", | ||
"lodash.clonedeep": "^2.4.1", | ||
"mocha": "^2.0.1", | ||
"vinyl-transform": "^1.0.0" | ||
}, | ||
"scripts": { | ||
"test": "gulp test" | ||
"test": "gulp test", | ||
"build": "gulp build" | ||
}, | ||
@@ -22,0 +27,0 @@ "repository": { |
548
README.md
@@ -0,5 +1,549 @@ | ||
[![Build Status](https://travis-ci.org/Yomguithereal/baobab.svg)](https://travis-ci.org/Yomguithereal/baobab) | ||
# Baobab | ||
JavaScript cursors for immutable data structures. | ||
**Baobab** is a JavaScript data tree supporting cursors and enabling developers to easily navigate and monitor nested data. | ||
**Pre-release 0.0.1** | ||
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 can be paired with React easily through mixins to provide a centralized model holding your application's state. | ||
## Installation | ||
If you want to use **Baobab** with node.js or browserify, you can use npm. | ||
```sh | ||
npm install baobab | ||
# Or for the latest dev version | ||
npm install git+https://github.com/Yomguithereal/baobab.git | ||
``` | ||
If you want to use it in the browser, just include the minified script located [here](https://raw.githubusercontent.com/Yomguithereal/baobab/master/build/baobab.min.js). | ||
```html | ||
<script src="baobab.min.js"></script> | ||
``` | ||
## 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) | ||
### Basics | ||
#### Instantiation | ||
Creating a *baobab* is as simple as instantiating it with an initial data set (note that only objects or array should be given). | ||
```js | ||
var Baobab = require('baobab'); | ||
var tree = new Baobab({hello: 'world'}); | ||
// Retrieving data from your tree | ||
tree.get(); | ||
>>> {hello: 'world'} | ||
``` | ||
#### Cursors | ||
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. | ||
```js | ||
// Considering the following tree | ||
var tree = new Baobab({ | ||
palette: { | ||
name: 'fancy', | ||
colors: ['blue', 'yellow', 'green'] | ||
} | ||
}); | ||
// Creating a cursor on the palette | ||
var paletteCursor = tree.select('palette'); | ||
paletteCursor.get(); | ||
>>> {name: 'fancy', colors: ['blue', 'yellow', 'green']} | ||
// Creating a cursor on the palette's colors | ||
var colorsCursor = tree.select('palette', 'colors'); | ||
colorsCursor.get(); | ||
>>> ['blue', 'yellow', 'green'] | ||
// Creating a cursor on the palette's third color | ||
var thirdColorCursor = tree.select('palette', 'colors', 2); | ||
thirdColorCursor.get(); | ||
>>> 'green' | ||
// Note you can also perform subselections if needed | ||
var colorCursor = paletteCursor.select('colors'); | ||
``` | ||
#### Updates | ||
A *baobab* tree can obviously be updated. However, one has to to understand that he won't do it, at least by default, synchronously. | ||
Rather, the tree will stack and merge every update order you give him and will only commit them at the next frame or next tick in node. | ||
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. | ||
##### Tree level | ||
*Setting a key* | ||
```js | ||
tree.set('hello', 'world'); | ||
``` | ||
##### Cursor level | ||
*Replacing data at cursor* | ||
```js | ||
cursor.set({hello: 'world'}); | ||
``` | ||
*Setting a key* | ||
```js | ||
cursor.set('hello', 'world'); | ||
``` | ||
*Pushing values* | ||
Obviously this will fail if target data is not an array. | ||
```js | ||
cursor.push('purple'); | ||
cursor.push(['purple', 'orange']); | ||
``` | ||
*Unshifting values* | ||
Obviously this will fail if target data is not an array. | ||
```js | ||
cursor.unshift('purple'); | ||
cursor.unshift(['purple', 'orange']); | ||
``` | ||
*Applying a function* | ||
```js | ||
cursor.apply(function(currentData) { | ||
return currentData + 1; | ||
}); | ||
``` | ||
#### Events | ||
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. | ||
Note however that only relevant cursors will be notified of data change. | ||
Events, can be bound to either the tree or cursors using the `on` method. | ||
*Example* | ||
```js | ||
// Considering the following tree | ||
var tree = new Baobab({ | ||
users: { | ||
john: { | ||
firstname: 'John', | ||
lastname: 'Silver' | ||
}, | ||
jack: { | ||
firstname: 'Jack', | ||
lastname: 'Gold' | ||
} | ||
} | ||
}); | ||
// And the following cursors | ||
var usersCusor = tree.select('users'), | ||
johnCursor = usersCursor.select('john'), | ||
jackCursor = usersCursor.select('jack'); | ||
// If we update the users | ||
usersCursor.update({ | ||
john: { | ||
firstname: {$set: 'John the third'} | ||
}, | ||
jack: { | ||
firstname: {$set: 'Jack the second'} | ||
} | ||
}); | ||
// Every cursor above will be notified of the update | ||
// But if we update only john | ||
johnCursor.set('firstname', 'John the third'); | ||
// Only the users and john cursor will be notified | ||
``` | ||
##### Tree level | ||
*update* | ||
Will fire if the tree is updated. | ||
```js | ||
tree.on('update', fn); | ||
``` | ||
*invalid* | ||
Will fire if a data-validation specification was passed at instance and if new data does not abide by those specifications. | ||
```js | ||
tree.on('invalid', fn); | ||
``` | ||
##### Cursor level | ||
*update* | ||
Will fire if data watched by cursor has updated. | ||
```js | ||
cursor.on('update', fn); | ||
``` | ||
*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 is irrelevant but becomes relevant again. | ||
```js | ||
cursor.on('relevant', fn); | ||
``` | ||
##### N.B. | ||
For more information concerning **Baobab**'s event emitting, see the [emmett](https://github.com/jacomyal/emmett) library. | ||
#### React mixins | ||
A *baobab* tree can easily be used as a UI model keeping the whole application state. | ||
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. | ||
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. | ||
##### Tree level | ||
You can bind a React component to the tree itself and register some handy cursors: | ||
```jsx | ||
var tree = new Baobab({ | ||
users: ['John', 'Jack'], | ||
information: { | ||
title: 'My fancy App' | ||
} | ||
}); | ||
// Single cursor | ||
var UserList = React.createClass({ | ||
mixins: [tree.mixin], | ||
cursor: ['users'], | ||
render: function() { | ||
var renderItem = function(name) { | ||
return <li>{name}</li>; | ||
}; | ||
return <ul>{this.cursor.get().map(renderItem)}</ul>; | ||
} | ||
}); | ||
// Multiple cursors | ||
var UserList = React.createClass({ | ||
mixins: [tree.mixin], | ||
cursors: [['users'], ['information', 'title']], | ||
render: function() { | ||
var renderItem = function(name) { | ||
return <li>{name}</li>; | ||
}; | ||
return ( | ||
<div> | ||
<h1>{this.cursors[1].get()}</h1> | ||
<ul>{this.cursor[0].get().map(renderItem)}</ul> | ||
</div> | ||
); | ||
} | ||
}); | ||
// Better multiple cursors | ||
var UserList = React.createClass({ | ||
mixins: [tree.mixin], | ||
cursors: { | ||
users: ['users'], | ||
title: ['information', 'title'] | ||
}, | ||
render: function() { | ||
var renderItem = function(name) { | ||
return <li>{name}</li>; | ||
}; | ||
return ( | ||
<div> | ||
<h1>{this.cursors.name.get()}</h1> | ||
<ul>{this.cursors.users.get().map(renderItem)}</ul> | ||
</div> | ||
); | ||
} | ||
}); | ||
``` | ||
##### Cursor level | ||
Else you can bind a single cursor to a React component | ||
```jsx | ||
var tree = new Baobab({users: ['John', 'Jack']}), | ||
usersCursor = tree.select('users'); | ||
var UserList = React.createClass({ | ||
mixins: [usersCursor.mixin], | ||
render: function() { | ||
var renderItem = function(name) { | ||
return <li>{name}</li>; | ||
}; | ||
return <ul>{this.cursor.get().map(renderItem)}</ul>; | ||
} | ||
}); | ||
``` | ||
### Advanced | ||
#### Polymorphisms | ||
If you ever need to, know that they are many ways to select and retrieve data within a *baobab*. | ||
```js | ||
var tree = new Baobab({ | ||
palette: { | ||
name: 'fancy', | ||
colors: ['blue', 'yellow', 'green'] | ||
} | ||
}); | ||
// Selecting | ||
var colorsCursor = tree.select('palette', 'colors'); | ||
var colorsCursor = tree.select(['palette', 'colors']); | ||
var colorsCursor = tree.select('palette').select('colors'); | ||
// Retrieving data | ||
colorsCursor.get(1) | ||
>>> 'yellow' | ||
paletteCursor.get('colors', 2) | ||
>>> 'green' | ||
tree.get('palette', 'colors'); | ||
tree.get(['palette', 'colors']); | ||
>>> ['blue', 'yellow', 'green'] | ||
``` | ||
#### Traversal | ||
*Going up in the tree* | ||
```js | ||
var tree = new Baobab({first: {second: 'yeah'}}) | ||
secondCursor = tree.select('first', 'second'); | ||
var firstCursor = secondCursor.up(); | ||
``` | ||
*Going left/right/down in lists* | ||
```js | ||
var tree = new Baobab({ | ||
list: [[1, 2], [3, 4]] | ||
}); | ||
var listCursor = tree.select('list'); | ||
listCursor.down().right().get(); | ||
>>> [3, 4] | ||
listCursor.select(1).down().left().get(); | ||
>>> 3 | ||
``` | ||
#### Options | ||
You can pass those options at instantiation. | ||
```js | ||
var baobab = new Baobab( | ||
// Initial data | ||
{ | ||
palette: { | ||
name: 'fancy', | ||
colors: ['blue', 'green'] | ||
} | ||
}, | ||
// Options | ||
{ | ||
maxHistory: 5, | ||
clone: true | ||
} | ||
) | ||
``` | ||
* **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. | ||
* **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. | ||
* **typology** *Typology|object*: a custom typology to be used to validate the tree's data. | ||
* **validate** *object*: a [typology](https://github.com/jacomyal/typology) schema ensuring the tree's data is valid. | ||
#### History | ||
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. | ||
*Example* | ||
```js | ||
var baobab = new Baobab({name: 'Maria'}, {maxHistory: 1}); | ||
baobab.set('name', 'Isabella'); | ||
// On next frame, when update has been committed | ||
baobab.get('name') | ||
>>> 'Isabella' | ||
baobab.undo(); | ||
baobab.get('name') | ||
>>> 'Maria' | ||
``` | ||
#### Update specifications | ||
If you ever need to specify complex updates without resetting 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), themselves inspired by [MongoDB](http://www.mongodb.org/)'s ones and can be used through `tree.update` and `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` | ||
*Example* | ||
```js | ||
var tree = new Baobab({ | ||
users: { | ||
john: { | ||
firstname: 'John', | ||
lastname: 'Silver' | ||
}, | ||
jack: { | ||
firstname: 'Jack', | ||
lastname: 'Gold' | ||
} | ||
} | ||
}); | ||
// From tree | ||
tree.update({ | ||
john: { | ||
firstname: { | ||
$set: 'John the 3rd' | ||
} | ||
}, | ||
jack: { | ||
firstname: { | ||
$apply: function(firstname) { | ||
return firstname + ' the 2nd'; | ||
} | ||
} | ||
} | ||
}); | ||
// From cursor | ||
var cursor = tree.select('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 | ||
``` | ||
#### Data validation | ||
WIP | ||
## Contribution | ||
Contributions are obviously welcome. This project is nothing but experimental and I would cherish some feedback and advice about the library. | ||
Be sure to add unit tests if relevant and pass them all before submitting your pull request. | ||
```bash | ||
# Installing the dev environment | ||
git clone git@github.com:Yomguithereal/baobab.git | ||
cd baobab | ||
npm install | ||
# Running the tests | ||
npm test | ||
# Linting, building | ||
gulp lint | ||
gulp build | ||
``` | ||
## License | ||
MIT |
@@ -7,7 +7,8 @@ /** | ||
*/ | ||
var Immutable = require('immutable'), | ||
Cursor = require('./cursor.js'), | ||
var Cursor = require('./cursor.js'), | ||
EventEmitter = require('emmett'), | ||
Typology = require('typology'), | ||
helpers = require('./helpers.js'), | ||
update = require('./update.js'), | ||
merge = require('./merge.js'), | ||
types = require('./typology.js'), | ||
@@ -26,3 +27,3 @@ mixins = require('./mixins.js'), | ||
if (!types.check(initialData, 'maplike')) | ||
if (!types.check(initialData, 'object')) | ||
throw Error('Baobab: invalid data.'); | ||
@@ -33,4 +34,8 @@ | ||
// Merging defaults | ||
this.options = merge(opts, defaults); | ||
this._cloner = this.options.cloningFunction || helpers.clone; | ||
// Properties | ||
this.data = Immutable.fromJS(initialData); | ||
this.data = this._cloner(initialData); | ||
@@ -41,6 +46,17 @@ // Privates | ||
this._history = []; | ||
this._registeredCursors = {}; | ||
// Merging defaults | ||
this.options = Immutable.fromJS(defaults).merge(opts); | ||
// Internal typology | ||
this.typology = this.options.typology ? | ||
(types.check(this.options.typology, 'typology') ? | ||
this.options.typology : | ||
new Typology(this.options.typology)) : | ||
new Typology(); | ||
// Internal validation | ||
this.validate = this.options.validate || null; | ||
if (!this.check()) | ||
throw Error('Baobab: instantiating with invalid data'); | ||
// Mixin | ||
@@ -56,14 +72,23 @@ this.mixin = mixins.baobab(this); | ||
Baobab.prototype._stack = function(spec) { | ||
var self = this; | ||
if (!types.check(spec, 'maplike')) | ||
if (!types.check(spec, 'object')) | ||
throw Error('Baobab.update: wrong specification.'); | ||
this._futureUpdate = helpers.merge(spec, this._futureUpdate); | ||
this._futureUpdate = merge(spec, this._futureUpdate); | ||
if (!this.options.get('delay')) | ||
return this._commit(); | ||
// Should we let the user commit? | ||
if (!this.options.autoCommit) | ||
return this; | ||
// Should we update synchronously? | ||
if (!this.options.delay) | ||
return this.commit(); | ||
// Updating asynchronously | ||
if (!this._willUpdate) { | ||
this._willUpdate = true; | ||
helpers.later(this._commit.bind(this)); | ||
helpers.later(function() { | ||
self.commit(); | ||
}); | ||
} | ||
@@ -74,17 +99,54 @@ | ||
Baobab.prototype._commit = function() { | ||
var self = this; | ||
Baobab.prototype._archive = function() { | ||
if (this.options.maxHistory <= 0) | ||
return; | ||
// Applying modification | ||
var result = update(this.data, this._futureUpdate); | ||
var record = { | ||
data: this._cloner(this.data) | ||
}; | ||
// Replacing data | ||
var oldData = this.data; | ||
this.data = result.data; | ||
// Replacing | ||
if (this._history.length === this.options.maxHistory) { | ||
this._history.pop(); | ||
} | ||
this._history.unshift(record); | ||
return record; | ||
}; | ||
/** | ||
* Prototype | ||
*/ | ||
Baobab.prototype.check = function() { | ||
return this.validate ? | ||
this.typology.check(this.data, this.validate) : | ||
true; | ||
}; | ||
Baobab.prototype.commit = function(referenceRecord) { | ||
var self = this, | ||
log; | ||
if (referenceRecord) { | ||
// Override | ||
this.data = referenceRecord.data; | ||
log = referenceRecord.log; | ||
} | ||
else { | ||
// Applying modification (mutation) | ||
var record = this._archive(); | ||
log = update(this.data, this._futureUpdate); | ||
if (record) | ||
record.log = log; | ||
} | ||
if (!this.check()) | ||
this.emit('invalid'); | ||
// Baobab-level update event | ||
this.emit('update', { | ||
oldData: oldData, | ||
newData: this.data, | ||
log: result.log | ||
log: log | ||
}); | ||
@@ -99,15 +161,28 @@ | ||
/** | ||
* Prototype | ||
*/ | ||
Baobab.prototype.select = function(path) { | ||
if (!path) | ||
throw Error('Baobab.select: invalid path.'); | ||
if (arguments.length > 1) | ||
path = Array.prototype.slice.call(arguments); | ||
path = helpers.arrayOf(arguments); | ||
if (!types.check(path, 'path')) | ||
throw Error('Baobab.select: invalid path.'); | ||
return new Cursor(this, path); | ||
// Casting to array | ||
path = (typeof path === 'string') ? [path] : path; | ||
// Registering a new cursor or giving the already existing one for path | ||
if (!this.options.cursorSingletons) { | ||
return new Cursor(this, path); | ||
} | ||
else { | ||
var hash = path.join('λ'); | ||
if (!this._registeredCursors[hash]) { | ||
var cursor = new Cursor(this, path); | ||
this._registeredCursors[hash] = cursor; | ||
return cursor; | ||
} | ||
else { | ||
return this._registeredCursors[hash]; | ||
} | ||
} | ||
}; | ||
@@ -119,15 +194,40 @@ | ||
if (arguments.length > 1) | ||
path = Array.prototype.slice.call(arguments); | ||
path = helpers.arrayOf(arguments); | ||
if (path) | ||
data = this.data.getIn(typeof path === 'string' ? [path] : path); | ||
data = helpers.getIn(this.data, typeof path === 'string' ? [path] : path); | ||
else | ||
data = this.data; | ||
if (this.options.get('toJS')) | ||
return data.toJS(); | ||
return this.options.clone ? this._cloner(data) : data; | ||
}; | ||
Baobab.prototype.reference = function(path) { | ||
var data; | ||
if (arguments.length > 1) | ||
path = helpers.arrayOf(arguments); | ||
if (path) | ||
data = helpers.getIn(this.data, typeof path === 'string' ? [path] : path); | ||
else | ||
return data; | ||
data = this.data; | ||
return data; | ||
}; | ||
Baobab.prototype.clone = function(path) { | ||
var data; | ||
if (arguments.length > 1) | ||
path = helpers.arrayOf(arguments); | ||
if (path) | ||
data = helpers.getIn(this.data, typeof path === 'string' ? [path] : path); | ||
else | ||
data = this.data; | ||
return this._cloner(data); | ||
}; | ||
Baobab.prototype.set = function(key, val) { | ||
@@ -148,14 +248,31 @@ | ||
Baobab.prototype.hasHistory = function() { | ||
return !!this._history.length; | ||
}; | ||
Baobab.prototype.getHistory = function() { | ||
return this._history; | ||
}; | ||
Baobab.prototype.undo = function() { | ||
if (!this.hasHistory()) | ||
throw Error('Baobab.undo: no history recorded, cannot undo.'); | ||
var lastRecord = this._history.shift(); | ||
this.commit(lastRecord); | ||
}; | ||
/** | ||
* Type definition | ||
*/ | ||
types.add('baobab', function(v) { | ||
return v instanceof Baobab; | ||
}); | ||
/** | ||
* Output | ||
*/ | ||
Baobab.prototype.toJS = function() { | ||
return this.data.toJS(); | ||
Baobab.prototype.toJSON = function() { | ||
return this.get(); | ||
}; | ||
Baobab.prototype.toJSON = Baobab.prototype.toJS; | ||
Baobab.prototype.toString = function() { | ||
return 'Baobab ' + this.data.toString().replace(/^[^{]+\{/, '{'); | ||
}; | ||
Baobab.prototype.inspect = Baobab.prototype.toString; | ||
Baobab.prototype.toSource = Baobab.prototype.toString; | ||
@@ -162,0 +279,0 @@ /** |
@@ -23,3 +23,2 @@ /** | ||
path = path || []; | ||
path = (typeof path === 'string') ? [path] : path; | ||
@@ -29,3 +28,3 @@ // Properties | ||
this.path = path; | ||
this.relevant = this.get() !== undefined; | ||
this.relevant = this.reference() !== undefined; | ||
@@ -63,3 +62,3 @@ // Root listeners | ||
// Handling relevancy | ||
var data = self.get() !== undefined; | ||
var data = self.reference() !== undefined; | ||
@@ -102,11 +101,8 @@ if (self.relevant) { | ||
Cursor.prototype.select = function(path) { | ||
if (!path) | ||
throw Error('precursors.Cursor.select: invalid path.'); | ||
if (arguments.length > 1) | ||
path = Array.prototype.slice.call(arguments); | ||
path = helpers.arrayOf(arguments); | ||
if (!types.check(path, 'path')) | ||
throw Error('precursors.Cursor.select: invalid path.'); | ||
return new Cursor(this.root, this.path.concat(path)); | ||
throw Error('baobab.Cursor.select: invalid path.'); | ||
return this.root.select(this.path.concat(path)); | ||
}; | ||
@@ -116,10 +112,37 @@ | ||
if (this.path.length) | ||
return new Cursor(this.root, this.path.slice(0, -1)); | ||
return this.root.select(this.path.slice(0, -1)); | ||
else | ||
return new Cursor(this.root, []); | ||
return this.root.select([]); | ||
}; | ||
Cursor.prototype.left = function() { | ||
var last = +this.path[this.path.length - 1]; | ||
if (isNaN(last)) | ||
throw Error('baobab.Cursor.left: cannot go left on a non-list type.'); | ||
return this.root.select(this.path.slice(0, -1).concat(last - 1)); | ||
}; | ||
Cursor.prototype.right = 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.'); | ||
return this.root.select(this.path.slice(0, -1).concat(last + 1)); | ||
}; | ||
Cursor.prototype.down = function() { | ||
var last = +this.path[this.path.length - 1]; | ||
if (!(this.reference() instanceof Array)) | ||
throw Error('baobab.Cursor.down: cannot descend on a non-list type.'); | ||
return this.root.select(this.path.concat(0)); | ||
}; | ||
Cursor.prototype.get = function(path) { | ||
if (arguments.length > 1) | ||
path = Array.prototype.slice.call(arguments); | ||
path = helpers.arrayOf(arguments); | ||
@@ -132,22 +155,62 @@ if (path) | ||
Cursor.prototype.set = function(value) { | ||
return this.update({$set: value}); | ||
Cursor.prototype.reference = function(path) { | ||
if (arguments.length > 1) | ||
path = helpers.arrayOf(arguments); | ||
if (path) | ||
return this.root.reference(this.path.concat(path)); | ||
else | ||
return this.root.reference(this.path); | ||
}; | ||
Cursor.prototype.push = function(value) { | ||
return this.update({$push: value}); | ||
Cursor.prototype.clone = function(path) { | ||
if (arguments.length > 1) | ||
path = helpers.arrayOf(arguments); | ||
if (path) | ||
return this.root.clone(this.path.concat(path)); | ||
else | ||
return this.root.clone(this.path); | ||
}; | ||
Cursor.prototype.unshift = function(value) { | ||
return this.update({$unshift: value}); | ||
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); | ||
} | ||
}; | ||
Cursor.prototype.append = function(value) { | ||
return this.update({$append: value}); | ||
Cursor.prototype.apply = function(fn) { | ||
if (typeof fn !== 'function') | ||
throw Error('baobab.Cursor.apply: argument is not a function.'); | ||
return this.update({$apply: fn}); | ||
}; | ||
Cursor.prototype.prepend = function(value) { | ||
return this.update({$prepend: value}); | ||
// TODO: maybe composing should be done here rather than in the merge | ||
Cursor.prototype.thread = function(fn) { | ||
if (typeof fn !== 'function') | ||
throw Error('baobab.Cursor.thread: argument is not a function.'); | ||
return this.update({$thread: fn}); | ||
}; | ||
Cursor.prototype.push = function(value) { | ||
if (arguments.length > 1) | ||
return this.update({$push: helpers.arrayOf(arguments)}); | ||
else | ||
return this.update({$push: value}); | ||
}; | ||
Cursor.prototype.unshift = function(value) { | ||
if (arguments.length > 1) | ||
return this.update({$unshift: helpers.arrayOf(arguments)}); | ||
else | ||
return this.update({$unshift: value}); | ||
}; | ||
Cursor.prototype.update = function(spec) { | ||
@@ -158,13 +221,14 @@ return this._stack(spec); | ||
/** | ||
* Type definition | ||
*/ | ||
types.add('cursor', function(v) { | ||
return v instanceof Cursor; | ||
}); | ||
/** | ||
* Output | ||
*/ | ||
Cursor.prototype.toJS = function() { | ||
return this.get().toJS(); | ||
Cursor.prototype.toJSON = function() { | ||
return this.get(); | ||
}; | ||
Cursor.prototype.toJSON = Cursor.prototype.toJS; | ||
Cursor.prototype.toString = function() { | ||
return 'Cursor ' + this.get().toString().replace(/^[^{]+\{/, '{'); | ||
}; | ||
Cursor.prototype.inspect = Cursor.prototype.toString; | ||
Cursor.prototype.toSource = Cursor.prototype.toString; | ||
@@ -171,0 +235,0 @@ /** |
@@ -7,42 +7,67 @@ /** | ||
*/ | ||
var Immutable = require('immutable'), | ||
types = require('typology'); | ||
var types = require('typology'); | ||
// Merge objects | ||
// TODO: optimize obviously... | ||
function merge() { | ||
var i, | ||
// Make a real array of an array-like object | ||
function arrayOf(o) { | ||
return Array.prototype.slice.call(o); | ||
} | ||
// Deep clone an object | ||
function clone(item) { | ||
if (!item) | ||
return item; | ||
var result, | ||
i, | ||
k, | ||
res = {}, | ||
l = arguments.length; | ||
l; | ||
for (i = l - 1; i >= 0; i--) | ||
for (k in arguments[i]) | ||
if (res[k] && types.check(arguments[i][k], 'object')) { | ||
if (types.check(item, 'array')) { | ||
result = []; | ||
for (i = 0, l = item.length; i < l; i++) | ||
result.push(clone(item[i])); | ||
if (('$push' in (res[k] || {})) && | ||
('$push' in arguments[i][k])) { | ||
if (types.check(res[k].$push, 'array')) | ||
res[k].$push = res[k].$push.concat(arguments[i][k].$push); | ||
else | ||
res[k].$push = [res[k].$push].concat(arguments[i][k].$push); | ||
} | ||
else if (('$unshift' in (res[k] || {})) && | ||
('$unshift' in arguments[i][k])) { | ||
if (types.check(arguments[i][k].$unshift, 'array')) | ||
res[k].$unshift = arguments[i][k].$unshift.concat(res[k].$unshift); | ||
else | ||
res[k].$unshift = [arguments[i][k].$unshift].concat(res[k].$unshift); | ||
} | ||
else { | ||
res[k] = merge(arguments[i][k], res[k]); | ||
} | ||
} | ||
else { | ||
res[k] = arguments[i][k]; | ||
} | ||
} else if (types.check(item, 'date')) { | ||
result = new Date(item.getTime()); | ||
return res; | ||
} 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; | ||
} | ||
return result; | ||
} | ||
// Simplistic composition | ||
function compose(fn1, fn2) { | ||
return function(arg) { | ||
return fn2(fn1(arg)); | ||
}; | ||
} | ||
// Retrieve nested objects | ||
function getIn(object, path) { | ||
path = path || []; | ||
var c = object, | ||
i, | ||
l; | ||
for (i = 0, l = path.length; i < l; i++) { | ||
if (typeof c[path[i]] === 'undefined') | ||
return; | ||
c = c[path[i]]; | ||
} | ||
return c; | ||
} | ||
// Return a fake object relative to the given path | ||
@@ -82,6 +107,9 @@ function pathObject(path, spec) { | ||
module.exports = { | ||
arrayOf: arrayOf, | ||
clone: clone, | ||
compose: compose, | ||
getIn: getIn, | ||
inherits: inherits, | ||
later: later, | ||
merge: merge, | ||
pathObject: pathObject | ||
}; |
@@ -51,3 +51,12 @@ /** | ||
// Making update handler | ||
this.__updateHandler = this.forceUpdate.bind(this); | ||
var fired = false; | ||
this.__updateHandler = (function() { | ||
if (!fired) { | ||
this.forceUpdate(); | ||
fired = true; | ||
setTimeout(function() { | ||
fired = false; | ||
}, 0); | ||
} | ||
}).bind(this); | ||
}, | ||
@@ -92,3 +101,5 @@ componentDidMount: function() { | ||
// Making update handler | ||
this.__updateHandler = this.forceUpdate.bind(this); | ||
this.__updateHandler = (function() { | ||
this.forceUpdate(); | ||
}).bind(this); | ||
}, | ||
@@ -95,0 +106,0 @@ componentDidMount: function() { |
@@ -7,26 +7,10 @@ /** | ||
*/ | ||
var Typology = require('typology'), | ||
Immutable = require('immutable'), | ||
Baobab = require('./baobab.js'), | ||
Cursor = require('./cursor.js'); | ||
var Typology = require('typology'); | ||
var typology = new Typology({ | ||
baobab: function(v) { | ||
return v instanceof Baobab; | ||
path: function(v) { | ||
return this.check(v, '?string|number') || this.check(v, ['string|number']); | ||
}, | ||
cursor: function(v) { | ||
return v instanceof Cursor; | ||
}, | ||
immutable: function(v) { | ||
return v instanceof Immutable.Iterable || this.check(v, 'primitive'); | ||
}, | ||
list: function(v) { | ||
return v instanceof Immutable.List; | ||
}, | ||
map: function(v) { | ||
return v instanceof Immutable.Map; | ||
}, | ||
maplike: 'object|map', | ||
path: function(v) { | ||
return this.check(v, '?string|number') || this.check(v, ['string']); | ||
typology: function(v) { | ||
return v instanceof Typology; | ||
} | ||
@@ -33,0 +17,0 @@ }); |
@@ -8,4 +8,3 @@ /** | ||
*/ | ||
var Immutable = require('immutable'), | ||
types = require('./typology.js'); | ||
var types = require('./typology.js'); | ||
@@ -17,3 +16,2 @@ var COMMANDS = {}; | ||
'$unshift', | ||
'$merge', | ||
'$apply' | ||
@@ -37,3 +35,3 @@ ].forEach(function(c) { | ||
var hash = path.join('$$'), | ||
var hash = path.join('λ'), | ||
fn, | ||
@@ -51,4 +49,4 @@ h, | ||
// Logging update | ||
if (!~log.indexOf(hash)) | ||
log.push(hash); | ||
if (hash && !log[hash]) | ||
log[hash] = true; | ||
@@ -80,12 +78,12 @@ // Applying | ||
if ('$set' in (spec[k] || {})) { | ||
h = hash ? hash + '$$' + k : k; | ||
h = hash ? hash + 'λ' + k : k; | ||
v = spec[k].$set; | ||
// Logging update | ||
if (!~log.indexOf(h)) | ||
log.push(h); | ||
if (h && !log[h]) | ||
log[h] = true; | ||
o[k] = v; | ||
} | ||
else if ('$apply' in (spec[k] || {})) { | ||
h = hash ? hash + '$$' + k : k; | ||
h = hash ? hash + 'λ' + k : k; | ||
fn = spec[k].$apply; | ||
@@ -97,6 +95,4 @@ | ||
// Logging update | ||
if (!~log.indexOf(h)) | ||
log.push(h); | ||
// NOTE: should we send an immutable variable here? | ||
if (h && !log[h]) | ||
log[h] = true; | ||
o[k] = fn.call(null, o[k]); | ||
@@ -123,17 +119,10 @@ } | ||
// Core function | ||
// NOTE: possible to achieve something better optimized through `asMutable`? | ||
function update(target, spec) { | ||
var o = target.toJS(), | ||
d = (spec.toJS) ? spec.toJS() : spec, | ||
log = [], | ||
k; | ||
var log = {}; | ||
mutator(log, o, d); | ||
mutator(log, target, spec); | ||
return { | ||
log: log.map(function(s) { | ||
return s.split('$$'); | ||
}), | ||
data: Immutable.fromJS(o) | ||
}; | ||
return Object.keys(log).map(function(hash) { | ||
return hash.split('λ'); | ||
}); | ||
} | ||
@@ -140,0 +129,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
38339
2
14
864
550
10
- Removedimmutable@^3.3.0
- Removedimmutable@3.8.2(transitive)