diff --git a/.gitignore b/.gitignore index fd4f2b0..53c6e4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules .DS_Store +TODO.md +*.log diff --git a/.npmignore b/.npmignore index d6e6c37..2a146cc 100644 --- a/.npmignore +++ b/.npmignore @@ -6,3 +6,4 @@ node_modules test/ build/ gulpfile.js +*.log diff --git a/.travis.yml b/.travis.yml index 587bd3e..18d1eb0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,4 @@ language: node_js +node_js: + - "node" + - "iojs" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..25ff960 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,90 @@ +# Changelog + +## v1.0.0 + +* Dropping `cursor.edit` and `cursor.remove` in favor of `cursor.set` and `cursor.unset` polymorphisms. +* Dropping `typology` dependency. +* Dropping options: `clone`, `cloningFunction`, `singletonCursors`, `shiftReferences`, `maxHistory`, `mixins` and `typology`. +* Updated `emmett` to `v3.0.0`. +* Moving react integration to [baobab-react](https://github.com/Yomguithereal/baobab-react). +* Shifting references is now default. +* Adding facets. +* Adding `$splice` keyword and `cursor.splice`. +* Adding `validationBehavior` option. +* Adding `$cursor` paths. +* Adding path polymorphisms to every cursor's setters. +* Reworking history to work at cursor level. +* Reworking validation process. +* Fixing some bugs. + +## v0.4.4 + +* Fixing `cursor.root`. +* Fixing `cursor.release`. +* Fixing build procedure for latest `node` and `browserify` versions. +* I9 support. + +## v0.4.3 + +* Adding React mixins function polymorphisms thanks to **@denisw**. +* Fixing `cursor.chain` thanks to **@jonypawks**. +* Fixing transaction flow issues thanks to **@jmisterka**. + +## v0.4.2 + +* Fixing deep object comparison and dynamic paths matching thanks to **@angus-c**. + +## v0.4.1 + +* Safer cursor update methods. +* Fixing `cursor.chain`. +* Fixing unset behavior when acting on lists. +* Fixing release methods. +* Path polymorphism for `tree/cursor.set`. +* Adding `tree/cursor.root` method. +* Reducing leak risks by making cursors and combinations lazier. + +## v0.4.0 + +* Several webpack-friendly changes. +* Fixing complex paths solving. +* Better `release` methods. +* Tree instantiation minimal polymorphism. +* Shooting gremlins in the head. +* Better internals. +* Implementing the `unset` and `remove` methods. + +## v0.3.2 + +* Bug fixes thanks to **@jacomyal**, **@jondot**. +* Better perfs thanks to **@christianalfoni**. +* `release` method for the tree. + +## v0.3.1 + +* Fixing reference shifting behaviours. +* `release` method for cursors. + +## v0.3.0 + +* Exposing `getIn` helper. +* Merged mixins are now executed after baobab's ones. +* Cursor combinations. +* Cursor data now available through component's state. +* Retrieval and selection sugar with functions and descriptive objects. +* Adding `referenceShifting` option. +* Cursor predicates. +* `$merge` command. +* Various optimizations and bug fixes. + +## v0.2.2 + +* Updating dependencies. +* Fixing several bugs. +* Better unit testing for mixins. +* `mixins` settings. +* Bower support. + +## v0.2.1 + +* Several bug fixes. diff --git a/README.md b/README.md index f0357a7..f97a162 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,11 @@ 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](#react-mixins) to provide a centralized model holding your application's state. +It aims at providing a centralized model holding an application's state and can be paired with **React** easily through mixins or higher order components (available [here](https://github.com/Yomguithereal/baobab-react)). -For a concise introduction about the library and how it can be used by a React/Flux application, you can head toward **@christianalfoni**'s [article](http://christianalfoni.github.io/javascript/2015/02/06/plant-a-baobab-tree-in-your-flux-application.html) on the subject. +For a concise introduction about the library and how it can be used in a React/Flux application, you can head toward **@christianalfoni**'s [article](http://christianalfoni.github.io/javascript/2015/02/06/plant-a-baobab-tree-in-your-flux-application.html) on the subject. + +**Fun fact**: A [Baobab](http://en.wikipedia.org/wiki/Adansonia_digitata), or *Adansonia digitata*, is a very big and magnificient African tree. ## Summary @@ -20,17 +22,17 @@ For a concise introduction about the library and how it can be used by a React/F * [Cursors](#cursors) * [Updates](#updates) * [Events](#events) - * [React mixins](#react-mixins) * [Advanced](#advanced) * [Polymorphisms](#polymorphisms) * [Traversal](#traversal) * [Options](#options) + * [Facets](#facets) * [History](#history) * [Update specifications](#update-specifications) * [Chaining mutations](#chaining-mutations) - * [Cursor combinations](#cursor-combinations) - * [Data validation](#data-validation) * [Common pitfalls](#common-pitfalls) +* [Philosophy](#philosophy) +* [Migration](#migration) * [Contribution](#contribution) * [License](#license) @@ -57,12 +59,12 @@ colorsCursor.push('orange'); ## Installation -If you want to use **Baobab** with node.js or browserify, you can use npm. +If you want to use **Baobab** with node.js/io.js or browserify/webpack etc., you can use npm. ```sh -npm install baobab +npm install baobab@1.0.0-rc1 -# Or for the latest dev version +# Or if you need the latest dev version npm install git+https://github.com/Yomguithereal/baobab.git ``` @@ -75,16 +77,18 @@ If you want to use it in the browser, just include the minified script located [ Or install with bower: ```js -bower install baobab +bower install baobab@1.0.0-rc1 ``` +The library (as a standalone) currently weights ~20ko minified and ~6ko gzipped. + ## Usage ### 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). +Creating a tree is as simple as instantiating *Baobab* with an initial data set. ```js var Baobab = require('baobab'); @@ -132,10 +136,24 @@ var colorCursor = paletteCursor.select('colors'); A *baobab* tree can obviously be updated. However, one has 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 it and will only commit them at the next frame or next tick in node. +Rather, the tree will stack and merge every update order you give it and will only commit them later on. 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 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* + +```js +var tree = new Baobab({hello: 'world'}); + +var initialState = tree.get(); +tree.set('hello', 'monde'}); + +// After asynchronous update... +assert(initialState !== tree.get()); +``` + ##### Tree level *Setting a key* @@ -155,64 +173,137 @@ tree.unset('hello'); *Replacing data at cursor* ```js -cursor.edit({hello: 'world'}); +cursor.set({hello: 'world'}); ``` *Setting a key* ```js cursor.set('hello', 'world'); + +// Nested path +cursor.set(['one', 'two'], 'world'); ``` *Removing data at cursor* ```js -cursor.remove(); +cursor.unset(); ``` *Unsetting a key* ```js cursor.unset('hello'); + +// Nested path +cursor.unset(['one', 'two']); ``` *Pushing values* -Obviously this will fail if target data is not an array. +Obviously this will fail if the value at cursor is not an array. ```js cursor.push('purple'); + +// Pushing several values cursor.push(['purple', 'orange']); + +// At key +cursor.push('list', 'orange') + +// Nested path +cursor.push(['one', 'two'], 'orange'); ``` *Unshifting values* -Obviously this will fail if target data is not an array. +Obviously this will fail if the value at cursor is not an array. ```js cursor.unshift('purple'); + +// Unshifting several values cursor.unshift(['purple', 'orange']); + +// At key +cursor.unshift('list', 'orange') + +// Nested path +cursor.unshift(['one', 'two'], 'orange'); +``` + +*Splicing an array* + +Obviously this will fail if the value at cursor is not an array. + +```js +cursor.splice([1, 1]); + +// Applying splice n times with different arguments +cursor.splice([[1, 2], [3, 2, 'hello']]); + +// At key +cursor.splice('list', [1, 1]) + +// Nested path +cursor.splice(['one', 'two'], [1, 1]); ``` *Applying a function* ```js -cursor.apply(function(currentData) { +var inc = function(currentData) { return currentData + 1; -}); +}; + +cursor.apply(inc); + +// At key +cursor.apply('number', inc) + +// Nested path +cursor.apply(['one', 'two'], 'orange'); +``` + +*Chaining functions through composition* + +For more details about this particular point, check [this](#chaining-mutations). + +```js +var inc = function(currentData) { + return currentData + 1; +}; + +cursor.chain(inc); + +// At key +cursor.chain('number', inc) + +// Nested path +cursor.chain(['one', 'two'], 'orange'); ``` *Shallowly merging objects* +Obviously this will fail if the value at cursor is not an object. + ```js cursor.merge({hello: 'world'}); + +// At key +cursor.merge('object', {hello: 'world'}) + +// Nested path +cursor.apply(['one', 'two'], {hello: 'world'}); ``` #### Events 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. -Note however that only relevant cursors will be notified of data change. +Note however that **only** relevant cursors will be notified of a change. Events can be bound to either the tree or cursors using the `on` method. @@ -255,22 +346,29 @@ johnCursor.set('firstname', 'John the third'); Will fire if the tree is updated. ```js -tree.on('update', fn); +tree.on('update', function(e) { + var affectedPaths = e.data.log, + previousState = e.data.previousState; + + //... +}); ``` *invalid* -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. +Will fire if the `validate` function (see [options](#options)) returned an error for the current update. ```js -tree.on('invalid', fn); +tree.on('invalid', function(e) { + console.log(e.data.error); +}); ``` ##### Cursor level *update* -Will fire if data watched by cursor has updated. +Will fire if data watched over by the cursor has updated. ```js cursor.on('update', fn); @@ -286,7 +384,7 @@ cursor.on('irrelevant', fn); *relevant* -Will fire if the cursor is irrelevant but becomes relevant again. +Will fire if the cursor was irrelevant but becomes relevant again. ```js cursor.on('relevant', fn); @@ -296,170 +394,6 @@ cursor.on('relevant', fn); 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 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. - -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 a Baoba tree cursors. Cursors bound to a component will have their values attached to `this.state.cursor` (or `this.state.cursors` if registering multiple cursors). - -###### Single cursor binding: - -Registering a cursor is as simple as defining a path. - -```jsx -var tree = new Baobab({ - users: ['John', 'Jack'], - information: { - title: 'My fancy App' - } -}); - -var UserList = React.createClass({ - mixins: [tree.mixin], - cursor: ['users'], - render: function() { - - // Cursor data is then available either through: - var data = this.cursor.get(); - // Or - var data = this.state.cursor; - - return ; - } -}); -``` - -You can also use a function returning a cursor. This allows the component to be bound to a cursor passed through `this.props`, for instance. - -```jsx -var tree = new Baobab({ - users: ['John', 'Jack'], - information: { - title: 'My fancy App' - } -}); - -var UserGroupList = React.createClass({ - mixins: [tree.mixin], - cursor: function() { - return this.props.usersCursor; - }, - render: function() { - return ; - } -} - -var users = tree.select('users'); -React.render( - , - document.getElementById('mount-point') -); -``` - -###### Multiple cursors binding: - -Similarly, to bind several cursors to a component, you can bind your component to a list of cursors. - -```jsx -var tree = new Baobab({ - users: ['John', 'Jack'], - information: { - title: 'My fancy App' - } -}); - -var UserList = React.createClass({ - mixins: [tree.mixin], - cursors: [['users'], ['information', 'title']], - render: function() { - return ( -
-

{this.cursors[1].get()}

- -
- ); - } -}); -``` - -To access cursors in an easier way, you can also bind your component to a map of cursors. - -```jsx -var tree = new Baobab({ - users: ['John', 'Jack'], - information: { - title: 'My fancy App' - } -}); - -var UserList = React.createClass({ - mixins: [tree.mixin], - cursors: { - users: ['users'], - title: ['information', 'title'] - }, - render: function() { - return ( -
-

{this.cursors.title.get()}

- -
- ); - } -}); -``` - -Same with a function: - -```jsx -var tree = new Baobab({ - users: ['John', 'Jack'], - information: { - title: 'My fancy App' - } -}); - -var UserList = React.createClass({ - mixins: [tree.mixin], - cursors: function() { - return { - users: this.props.usersCursor, - title: ['information', 'title'] - } - }, - render: function() { - return ( -
-

{this.cursors.title.get()}

- -
- ); - } -}); - -var users = tree.select('users'); -React.render( - , - document.getElementById('mount-point') -); -``` - ### Advanced #### Polymorphisms @@ -471,6 +405,7 @@ var tree = new Baobab({ palette: { name: 'fancy', colors: ['blue', 'yellow', 'green'], + currentColor: 1, items: [{id: 'one', value: 'Hey'}, {id: 'two', value: 'Ho'}] } }); @@ -481,10 +416,10 @@ var colorsCursor = tree.select(['palette', 'colors']); var colorsCursor = tree.select('palette').select('colors'); // Retrieving data -colorsCursor.get(1) +colorsCursor.get(1); >>> 'yellow' -paletteCursor.get('colors', 2) +paletteCursor.get('colors', 2); >>> 'green' tree.get('palette', 'colors'); @@ -505,6 +440,17 @@ tree.get('palette', 'colors', function(color) { var complexCursor = tree.select('items', {id: 'one'}, 'value'); tree.get('items', {id: 'one'}, 'value'); >>> 'Hey' + +// Retrieving or selecting data by using the value of another cursor +var currentColorCursor = tree.select('colors', {$cursor: ['currentColor']}); + +var currentColor = tree.get('colors', {$cursor: ['currentColor']}); + +// Creating a blank tree +var blankTree = new Baobab(); + +// You despise "new"? +var tree = Baobab(); ``` #### Traversal @@ -548,12 +494,12 @@ twoCursor.rightmost().get(); var tree = new Baobab({first: {second: 'yeah'}}), cursor = tree.select('first'); -var rootCursor = tree.root(); +var rootCursor = tree.root; // or var rootCursor = cursor.root(); ``` -*Check information about the cursor's location in the tree* +*Checking information about the cursor's location in the tree* ```js cursor.isRoot(); @@ -578,60 +524,200 @@ var baobab = new Baobab( // Options { - maxHistory: 5, - clone: true + autoCommit: false } ) ``` * **autoCommit** *boolean* [`true`]: should the tree auto commit updates or should it let the user do so through the `commit` method? * **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`. -* **maxHistory** *number* [`0`]: max number of records the tree is allowed to store within its internal history. -* **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. -* **validate** *object*: a [typology](https://github.com/jacomyal/typology) schema ensuring the tree's data is valid. +* **facets** *object*: a collection of facets to register when the tree is istantiated. For more information, see [facets](#facets). +* **validate** *function*: a function in charge of validating the tree whenever it updates. See below for an example of such function. +* **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. + +*Validation function* + +```js +function validationFunction(previousState, newState, affectedPaths) { + // Peform validation here and return an error if + // the tree is invalid + if (!valid) + return new Error('Invalid tree because of reasons.'); +} + +var tree = new Baobab({...}, {validate: validationFunction}); +``` + +#### 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(); + +// Listening +facet.on('update', function() { + console.log('New value:', facet.get()); +}); +``` #### History -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. +**Baobab** lets you record the state of any cursor so you can seamlessly implement undo/redo features. *Example* ```js -var baobab = new Baobab({name: 'Maria'}, {maxHistory: 1}); +// Asynchronous tree so that examples are simpler +var baobab = new Baobab({colors: ['blue']}, {asynchronous: false}), + cursor = baobab.select('colors'); + +// Starting to record state, with 10 records maximum +cursor.startRecording(10); + +cursor.push('yellow'); +cursor.push('purple'); +cursor.push('orange'); + +cursor.get(); +>>> ['blue', 'yellow', 'purple', 'orange'] + +cursor.undo(); +cursor.get(); +>>> ['blue', 'yellow', 'purple'] + +cursor.undo(2); +cursor.get(); +>>> ['blue'] +``` + +*Starting recording* + +Default max number of records is 5. + +```js +cursor.startRecording(maxNbOfRecords); +``` + +*Stoping recording* + +```js +cursor.stopRecording(); +``` + +*Undoing* + +```js +cursor.undo(); +cursor.undo(nbOfSteps); +``` + +*Clearing history* + +```js +cursor.clearHistory(); +``` -baobab.set('name', 'Isabella'); +*Checking if the cursor has an history* -// On next frame, when update has been committed -baobab.get('name') ->>> 'Isabella' -baobab.undo(); -baobab.get('name') ->>> 'Maria' +```js +cursor.hasHistory(); ``` -*Related Methods* +*Checking whether the cursor is currently recording* ```js -// Check whether our tree hold records -baobab.hasHistory(); ->>> true +cursor.recording; +``` -// Retrieving history records -baobab.getHistory(); +*Retrieving the cursor's history* + +```js +cursor.getHistory(); ``` #### 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. +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), themselves inspired by [MongoDB](http://www.mongodb.org/)'s ones and can be used through `tree.update` and `cursor.update`. +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* +**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. @@ -642,6 +728,7 @@ The available commands are the following and are basically the same as the curso * `$chain` * `$push` * `$unshift` +* `$splice` * `$merge` * `$unset` @@ -708,100 +795,8 @@ cursor.chain(inc); // will produce 3 ``` -#### 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 - -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); -}); -``` - #### 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 ; - } -}); -``` - *Immutable behaviour* TL;DR: Don't mutate things in your *baobab* tree. Let the tree handle its own mutations. @@ -821,6 +816,35 @@ tree.set('key', o); o.hello = 'other world'; ``` +## Philosophy + +**UIs 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. + +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. + +**Only data should enter the tree** + +You shouldn't try to shove anything else than raw data into the tree. The tree hasn't been conceived to hold classes or fancy indexes with many circular references and cannot perform its magic on it. But, probably such magic is not desirable for those kind of abstractions anyway. + +That is to say the data you insert into the tree should logically be JSON-serializable else you might be missing the point. + +## Migration + +**From v0.4.x to 1.0.0** + +A lot of changes occurred between `0.4.x` and `1.0.0`. Most notable changes being the following ones: + +* The tree now shift references by default. +* React integration has improved and is now handled by [baobab-react](https://github.com/Yomguithereal/baobab-react). +* `cursor.edit` and `cursor.remove` have been replaced by `cursor.set` and `cursor.unset` single argument polymorphisms. +* A lot of options (now unnecessary) have been dropped. +* Validation is no longer handled by [`typology`](https://github.com/jacomyal/typology) so you can choose you own validation system and so the library can remain lighter. +* Some new features such as: `$splice`, facets and so on... + +For more information, see the [changelog](./CHANGELOG.md). + ## Contribution Contributions are obviously welcome. This project is nothing but experimental and I would cherish some feedback and advice about the library. diff --git a/bower.json b/bower.json index a55acc9..f9bc515 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "baobab", "main": "build/baobab.min.js", - "version": "0.4.4", + "version": "1.0.0", "homepage": "https://github.com/Yomguithereal/baobab", "author": { "name": "Guillaume Plique", diff --git a/build/baobab.min.js b/build/baobab.min.js index 34eda33..b82e51c 100644 --- a/build/baobab.min.js +++ b/build/baobab.min.js @@ -1,2 +1,2 @@ -/* baobab.js - Version: 0.4.4 - Author: Yomguithereal (Guillaume Plique) */ -!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Baobab=t()}}(function(){var t;return function e(t,r,n){function o(s,a){if(!r[s]){if(!t[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(i)return i(s,!0);var h=new Error("Cannot find module '"+s+"'");throw h.code="MODULE_NOT_FOUND",h}var c=r[s]={exports:{}};t[s][0].call(c.exports,function(e){var r=t[s][1][e];return o(r?r:e)},c,c.exports,e,t,r,n)}return r[s].exports}for(var i="function"==typeof require&&require,s=0;se;e++)n.push(t[e].handler);return n}var o={once:"boolean",scope:"object"},i=function(){this._enabled=!0,this._children=[],this._handlers={},this._handlersAll=[]};i.prototype.on=function(t,e,r){var n,s,a,u,h,c;if("function"==typeof e){for(h="string"==typeof t?[t]:t,n=0,s=h.length;n!==s;n+=1)if(u=h[n]){this._handlers[u]||(this._handlers[u]=[]),c={handler:e};for(a in r||{}){if(!o[a])throw new Error('The option "'+a+'" is not recognized by Emmett.');c[a]=r[a]}this._handlers[u].push(c)}}else if(t&&"object"==typeof t&&!Array.isArray(t))for(u in t)i.prototype.on.call(this,u,t[u],e);else{if("function"!=typeof t)throw new Error("Wrong arguments.");c={handler:t};for(a in r||{}){if(!o[a])throw new Error('The option "'+a+'" is not recognized by Emmett.');c[a]=r[a]}this._handlersAll.push(c)}return this},i.prototype.once=function(t,e,r){if("function"==typeof e)r=r||{},r.once=!0,this.on(t,e,r);else{if((!t||"object"!=typeof t||Array.isArray(t))&&"function"!=typeof t)throw new Error("Wrong arguments.");e=e||{},e.once=!0,this.on(t,e)}return this},i.prototype.off=function(t,e){var r,n,o,i,s,a,u,h="string"==typeof t?[t]:t;if(1===arguments.length&&"function"==typeof h){e=arguments[0];for(s in this._handlers){for(a=[],r=0,n=this._handlers[s].length;r!==n;r+=1)this._handlers[s][r].handler!==e&&a.push(this._handlers[s][r]);this._handlers[s]=a}for(a=[],r=0,n=this._handlersAll.length;r!==n;r+=1)this._handlersAll[r].handler!==e&&a.push(this._handlersAll[r]);this._handlersAll=a}else if(2===arguments.length)for(r=0,n=h.length;r!==n;r+=1){if(u=h[r],this._handlers[u]){for(a=[],o=0,i=this._handlers[u].length;o!==i;o+=1)this._handlers[u][o].handler!==e&&a.push(this._handlers[u][o]);this._handlers[u]=a}this._handlers[u]&&0===this._handlers[u].length&&delete this._handlers[u]}return this},i.prototype.unbindAll=function(){var t;this._handlersAll=[];for(t in this._handlers)delete this._handlers[t];return this},i.prototype.emit=function(t,e){var r,n,o,i,s,a,u,h,c,l,f="string"==typeof t?[t]:t;if(!this._enabled)return this;for(e=void 0===e?{}:e,r=0,n=f.length;r!==n;r+=1)if(l=f[r],c=(this._handlers[l]||[]).concat(this._handlersAll),c.length){for(u={type:l,data:e||{},target:this},a=[],o=0,i=c.length;o!==i;o+=1)(this._handlers[l]&&this._handlers[l].indexOf(c[o])>=0||this._handlersAll.indexOf(c[o])>=0)&&(c[o].handler.call("scope"in c[o]?c[o].scope:this,u),c[o].once&&a.push(c[o]));for(s=0;sr;r++)if(t._children[r]===e){t._children.splice(r,1);break}}),this._children.push(e),e},i.prototype.listeners=function(t){var r,n,o,i=[];if(t)for(i=e(this._handlers[t]),n=0,o=this._children.length;o>n;n++)i=i.concat(this._children[n].listeners(t));else{i=e(this._handlersAll);for(r in this._handlers)i=i.concat(e(this._handlers[r]));for(n=0,o=this._children.length;o>n;n++)i=i.concat(this._children[n].listeners())}return i},i.prototype.kill=function(){if(this.emit("emmett:kill"),this.unbindAll(),this._handlers=null,this._handlersAll=null,this._enabled=!1,this._children)for(var t=0,e=this._children.length;e>t;t++)this._children[t].kill();this._children=null},i.prototype.disable=function(){return this._enabled=!1,this},i.prototype.enable=function(){return this._enabled=!0,this},i.version="2.1.2","undefined"!=typeof n?("undefined"!=typeof r&&r.exports&&(n=r.exports=i),n.Emitter=i):"function"==typeof t&&t.amd?t("emmett",[],function(){return i}):this.Emitter=i}).call(this)},{}],4:[function(e,r,n){!function(){"use strict";function e(t){function e(t,o){var s,a,u,h,c,l,f,p,d=!1,y=!1,g=r.get(t);if("string"===r.get(o)){for(s=o.replace(/^[\?\!]/,"").split(/\|/),u=s.length,a=0;u>a;a++)if(i.indexOf(s[a])<0&&!(s[a]in n))throw new Error("Invalid type.");if(o.match(/^\?/)&&(d=!0),o.replace(/^\?/,"").match(/^\!/)&&(y=!0),y&&d)throw new Error("Invalid type.");for(a in s)if(n[s[a]]&&("function"==typeof n[s[a]].type?n[s[a]].type.call(r,t)===!0:!e(t,n[s[a]].type)))return y?(c=new Error,c.message='Expected a "'+o+'" but found a "'+s[a]+'".',c.expected=o,c.type=s[a],c.value=t,c):null;return null===t||void 0===t?y||d?null:(c=new Error,c.message='Expected a "'+o+'" but found a "'+g+'".',c.expected=o,c.type=g,c.value=t,c):(f=~s.indexOf("*"),p=~s.indexOf(g),y&&(f||p)?(c=new Error,c.message='Expected a "'+o+'" but found a "'+(p?g:"*")+'".',c.type=p?g:"*",c.expected=o,c.value=t,c):y||f||p?null:(c=new Error,c.message='Expected a "'+o+'" but found a "'+g+'".',c.expected=o,c.type=g,c.value=t,c))}if("object"===r.get(o)){if("object"!==g)return c=new Error,c.message='Expected an object but found a "'+g+'".',c.expected=o,c.type=g,c.value=t,c;for(h in o)if(l=e(t[h],o[h]))return c=l,c.path=c.path?[h].concat(c.path):[h],c;for(h in t)if(void 0===o[h])return c=new Error,c.message='Unexpected key "'+h+'".',c.type=g,c.value=t,c;return null}if("array"===r.get(o)){if(1!==o.length)throw new Error("Invalid type.");if("array"!==g)return c=new Error,c.message='Expected an array but found a "'+g+'".',c.expected=o,c.type=g,c.value=t,c;for(u=t.length,a=0;u>a;a++)if(l=e(t[a],o[0]))return c=l,c.path=c.path?[a].concat(c.path):[a],c;return null}throw new Error("Invalid type.")}var r=this,n={};if(this.add=function(t,e){var r,o,s,a,u,h;if(1===arguments.length){if("object"!==this.get(t))throw new Error("If types.add is called with one argument, this one has to be an object.");r=t,a=r.id,h=r.type}else{if(2!==arguments.length)throw new Error("types.add has to be called with one or two arguments.");if("string"!=typeof t||!t)throw new Error("If types.add is called with more than one argument, the first one must be the string id.");a=t,h=e}if("string"!==this.get(a)||0===a.length)throw new Error("A type requires an string id.");if(void 0!==n[a]&&"proto"!==n[a])throw new Error('The type "'+a+'" already exists.');if(~i.indexOf(a))throw new Error('"'+a+'" is a reserved type name.');n[a]=1,s=(r||{}).proto||[],s=Array.isArray(s)?s:[s],u={};for(o in s)void 0===n[s[o]]&&(n[s[o]]=1,u[s[o]]=1);if("function"!==this.get(h)&&!this.isValid(h))throw new Error("A type requires a valid definition. This one can be a preexistant type or else a function testing given objects.");if(n[a]=void 0===r?{id:a,type:h}:{},void 0!==r)for(o in r)n[a][o]=r[o];for(o in u)o!==a&&delete n[o];return this},this.has=function(t){return!!n[t]},this.get=function(t){return null===t||void 0===t?String(t):o[Object.prototype.toString.call(t)]||"object"},this.check=function(t,r,n){var o=e(t,r);if(n&&o)throw o;return!o},this.isValid=function(t){var e,r,o;if("string"===this.get(t)){e=t.replace(/^[\?\!]/,"").split(/\|/);for(o in e)if(i.indexOf(e[o])<0&&!(e[o]in n))return!1;return!0}if("object"===this.get(t)){for(r in t)if(!this.isValid(t[r]))return!1;return!0}return"array"===this.get(t)&&1===t.length?this.isValid(t[0]):!1},this.add("type",function(t){return this.isValid(t)}.bind(this)),this.add("primitive",function(t){return!t||!(t instanceof Object||"object"==typeof t)}),t=t||{},"object"!==this.get(t))throw Error("Invalid argument.");for(var s in t)this.add(s,t[s])}var o={},i=["*"];!function(){var t,e,r=["Arguments","Boolean","Number","String","Function","Array","Date","RegExp","Object"];for(t in r)e=r[t],i.push(e.toLowerCase()),o["[object "+e+"]"]=e.toLowerCase()}();var s=e;e.call(s),Object.defineProperty(s,"version",{value:"0.3.1"}),"undefined"!=typeof n?("undefined"!=typeof r&&r.exports&&(n=r.exports=s),n.types=s):"function"==typeof t&&t.amd?t("typology",[],function(){return s}):this.types=s}(this)},{}],5:[function(t,e){function r(t){return t+"$"+(new Date).getTime()+(""+Math.random()).replace("0.","")}function n(t,e){if(arguments.length<1&&(t={}),!(this instanceof n))return new n(t,e);if(!f.Object(t)&&!f.Array(t))throw Error("Baobab: invalid data.");if(i.call(this),this.options=a.shallowMerge(l,e),this._cloner=this.options.cloningFunction||a.deepClone,this._transaction={},this._future=void 0,this._history=[],this._cursors={},this.typology=this.options.typology?this.options.typology instanceof s?this.options.typology:new s(this.options.typology):new s,this.validate=this.options.validate||null,this.validate)try{this.typology.check(t,this.validate,!0)}catch(r){throw r.message="/"+r.path.join("/")+": "+r.message,r}this.data=this._cloner(t),this.mixin=c.baobab(this)}var o=t("./cursor.js"),i=t("emmett"),s=t("typology"),a=t("./helpers.js"),u=t("./update.js"),h=t("./merge.js"),c=t("./mixins.js"),l=t("../defaults.js"),f=t("./type.js");a.inherits(n,i),n.prototype._archive=function(){if(!(this.options.maxHistory<=0)){var t={data:this._cloner(this.data)};return this._history.length===this.options.maxHistory&&this._history.pop(),this._history.unshift(t),t}},n.prototype.commit=function(t){var e;if(t)this.data=t.data,e=t.log;else{this.options.shiftReferences&&(this.data=a.shallowClone(this.data));var r=this._archive();e=u(this.data,this._transaction,this.options),r&&(r.log=e)}if(this.validate){var n,o,i=[],s=e.length;for(o=0;s>o;o++)if(n=a.getIn(this.validate,e[o]))try{this.typology.check(this.get(e[o]),n,!0)}catch(h){h.path=e[o].concat(h.path||[]),i.push(h)}i.length&&this.emit("invalid",{errors:i})}return this._transaction={},this._future&&(this._future=clearTimeout(this._future)),this.emit("update",{log:e}),this},n.prototype.select=function(t){if(!t)throw Error("Baobab.select: invalid path.");if(arguments.length>1&&(t=a.arrayOf(arguments)),!f.Path(t))throw Error("Baobab.select: invalid path.");t=f.Array(t)?t:[t];var e,n=f.ComplexPath(t);if(n&&(e=a.solvePath(this.data,t)),this.options.cursorSingletons){var i=t.map(function(t){return f.Function(t)?r("fn"):f.Object(t)?r("ob"):t}).join("λ");if(this._cursors[i])return this._cursors[i];var s=new o(this,t,e,i);return this._cursors[i]=s,s}return new o(this,t)},n.prototype.root=function(){return this.select([])},n.prototype.reference=function(t){if(arguments.length>1&&(t=a.arrayOf(arguments)),!f.Path(t))throw Error("Baobab.get: invalid path.");return a.getIn(this.data,f.String(t)||f.Number(t)?[t]:t)},n.prototype.get=function(){var t=this.reference.apply(this,arguments);return this.options.clone?this._cloner(t):t},n.prototype.clone=function(){return this._cloner(this.reference.apply(this,arguments))},n.prototype.set=function(t,e){if(arguments.length<2)throw Error("Baobab.set: expects a key and a value.");var r={};if(f.Array(t)){var n=a.solvePath(this.data,t);if(!n)throw Error("Baobab.set: could not solve dynamic path.");r=a.pathObject(n,{$set:e})}else r[t]={$set:e};return this.update(r)},n.prototype.unset=function(t){if(!t&&0!==t)throw Error("Baobab.unset: expects a valid key to unset.");var e={};return e[t]={$unset:!0},this.update(e)},n.prototype.update=function(t){var e=this;if(!f.Object(t))throw Error("Baobab.update: wrong specification.");return this._transaction=h(t,this._transaction),this.options.autoCommit?this.options.asynchronous?(this._future||(this._future=setTimeout(e.commit.bind(e,null),0)),this):this.commit():this},n.prototype.hasHistory=function(){return!!this._history.length},n.prototype.getHistory=function(){return this._history},n.prototype.undo=function(){if(!this.hasHistory())throw Error("Baobab.undo: no history recorded, cannot undo.");var t=this._history.shift();this.commit(t)},n.prototype.release=function(){delete this.data,delete this._transaction,delete this._history;for(var t in this._cursors)this._cursors[t].release();delete this._cursors,this.kill()},n.prototype.toJSON=function(){return this.reference()},e.exports=n},{"../defaults.js":2,"./cursor.js":7,"./helpers.js":8,"./merge.js":9,"./mixins.js":10,"./type.js":11,"./update.js":12,emmett:3,typology:4}],6:[function(t,e){function r(t,e){e.on("update",t.cursorListener),t.tree.off("update",t.treeListener),t.tree.on("update",t.treeListener)}function n(t){var e=this;if(arguments.length<2)throw Error("baobab.Combination: not enough arguments.");var n=arguments[1],o=s.arrayOf(arguments).slice(2);if(n instanceof Array&&(o=n.slice(1),n=n[0]),!a.Cursor(n))throw Error("baobab.Combination: argument should be a cursor.");if("or"!==t&&"and"!==t)throw Error("baobab.Combination: invalid operator.");i.call(this),this.cursors=[n],this.operators=[],this.tree=n.tree,this.updates=new Array(this.cursors.length),this.cursorListener=function(){e.updates[e.cursors.indexOf(this)]=!0},this.treeListener=function(){var t,r,n=e.updates[0];for(t=1,r=e.cursors.length;r>t;t++)n="or"===e.operators[t-1]?n||e.updates[t]:n&&e.updates[t];n&&e.emit("update"),e.updates=new Array(e.cursors.length)},this.bound=!1;var u=this.on,h=this.once,c=function(){e.bound||(e.bound=!0,e.cursors.forEach(function(t){r(e,t)}))};this.on=function(){return c(),u.apply(this,arguments)},this.once=function(){return c(),h.apply(this,arguments)},o.forEach(function(e){this[t](e)},this)}function o(t){n.prototype[t]=function(e){if(!a.Cursor(e))throw this.release(),Error("baobab.Combination."+t+": argument should be a cursor.");if(~this.cursors.indexOf(e))throw this.release(),Error("baobab.Combination."+t+": cursor already in combination.");return this.cursors.push(e),this.operators.push(t),this.updates.length++,this.bound&&r(this,e),this}}var i=t("emmett"),s=t("./helpers.js"),a=t("./type.js");s.inherits(n,i),o("or"),o("and"),n.prototype.release=function(){this.cursors.forEach(function(t){t.off("update",this.cursorListener)},this),this.tree.off("update",this.treeListener),this.cursors=null,this.operators=null,this.tree=null,this.updates=null,this.kill()},e.exports=n},{"./helpers.js":8,"./type.js":11,emmett:3}],7:[function(t,e){function r(t,e,r,o){var a=this;n.call(this),e=e||[],this.tree=t,this.path=e,this.hash=o,this.relevant=void 0!==this.reference(),this.complexPath=!!r,this.solvedPath=this.complexPath?r:this.path,this.updateHandler=function(t){var e,r,n,o,i,u,h=t.data.log,c=!1;if(a.complexPath&&(a.solvedPath=s.solvePath(a.tree.data,a.path)),!a.path.length)return a.emit("update");t:for(i=0,n=h.length;n>i;i++)for(e=h[i],u=0,o=e.length;o>u&&(r=e[u],r===""+a.solvedPath[u]);u++)if(u+1===o||u+1===a.solvedPath.length){c=!0;break t}var l=void 0!==a.reference();a.relevant?l&&c?a.emit("update"):l||(a.emit("irrelevant"),a.relevant=!1):l&&c&&(a.emit("relevant"),a.emit("update"),a.relevant=!0)},this.mixin=i.cursor(this);var u=!1,h=this.on,c=this.once,l=function(){u||(u=!0,a.tree.on("update",a.updateHandler))};this.on=function(){return l(),h.apply(this,arguments)},this.once=function(){return l(),c.apply(this,arguments)}}var n=t("emmett"),o=t("./combination.js"),i=t("./mixins.js"),s=t("./helpers.js"),a=t("./type.js");s.inherits(r,n),r.prototype.isRoot=function(){return!this.path.length},r.prototype.isLeaf=function(){return a.Primitive(this.reference())},r.prototype.isBranch=function(){return!this.isLeaf()&&!this.isRoot()},r.prototype.root=function(){return this.tree.root()},r.prototype.select=function(t){if(arguments.length>1&&(t=s.arrayOf(arguments)),!a.Path(t))throw Error("baobab.Cursor.select: invalid path.");return this.tree.select(this.path.concat(t))},r.prototype.up=function(){return this.solvedPath&&this.solvedPath.length?this.tree.select(this.path.slice(0,-1)):null},r.prototype.left=function(){var t=+this.solvedPath[this.solvedPath.length-1];if(isNaN(t))throw Error("baobab.Cursor.left: cannot go left on a non-list type.");return t?this.tree.select(this.solvedPath.slice(0,-1).concat(t-1)):null},r.prototype.leftmost=function(){var t=+this.solvedPath[this.solvedPath.length-1];if(isNaN(t))throw Error("baobab.Cursor.leftmost: cannot go left on a non-list type.");return this.tree.select(this.solvedPath.slice(0,-1).concat(0))},r.prototype.right=function(){var t=+this.solvedPath[this.solvedPath.length-1];if(isNaN(t))throw Error("baobab.Cursor.right: cannot go right on a non-list type.");return t+1===this.up().reference().length?null:this.tree.select(this.solvedPath.slice(0,-1).concat(t+1))},r.prototype.rightmost=function(){var t=+this.solvedPath[this.solvedPath.length-1];if(isNaN(t))throw Error("baobab.Cursor.right: cannot go right on a non-list type.");var e=this.up().reference();return this.tree.select(this.solvedPath.slice(0,-1).concat(e.length-1))},r.prototype.down=function(){+this.solvedPath[this.solvedPath.length-1];return this.reference()instanceof Array?this.tree.select(this.solvedPath.concat(0)):null},r.prototype.get=function(t){return arguments.length>1&&(t=s.arrayOf(arguments)),this.tree.get(a.Step(t)?this.solvedPath.concat(t):this.solvedPath)},r.prototype.reference=function(t){return arguments.length>1&&(t=s.arrayOf(arguments)),this.tree.reference(a.Step(t)?this.solvedPath.concat(t):this.solvedPath)},r.prototype.clone=function(t){return arguments.length>1&&(t=s.arrayOf(arguments)),this.tree.clone(a.Step(t)?this.solvedPath.concat(t):this.solvedPath)},r.prototype.set=function(t,e){if(arguments.length<2)throw Error("baobab.Cursor.set: expecting at least key/value.");var r=this.reference();if("object"!=typeof r)throw Error("baobab.Cursor.set: trying to set key to a non-object.");var n={};if(a.Array(t)){var o=s.solvePath(r,t);if(!o)throw Error("baobab.Cursor.set: could not solve dynamic path.");n=s.pathObject(o,{$set:e})}else n[t]={$set:e};return this.update(n)},r.prototype.edit=function(t){return this.update({$set:t})},r.prototype.unset=function(t){if(!t&&0!==t)throw Error("baobab.Cursor.unset: expects a valid key to unset.");if("object"!=typeof this.reference())throw Error("baobab.Cursor.set: trying to set key to a non-object.");var e={};return e[t]={$unset:!0},this.update(e)},r.prototype.remove=function(){if(this.isRoot())throw Error("baobab.Cursor.remove: cannot remove root node.");return this.update({$unset:!0})},r.prototype.apply=function(t){if("function"!=typeof t)throw Error("baobab.Cursor.apply: argument is not a function.");return this.update({$apply:t})},r.prototype.chain=function(t){if("function"!=typeof t)throw Error("baobab.Cursor.chain: argument is not a function.");return this.update({$chain:t})},r.prototype.push=function(t){if(!(this.reference()instanceof Array))throw Error("baobab.Cursor.push: trying to push to non-array value.");return this.update(arguments.length>1?{$push:s.arrayOf(arguments)}:{$push:t})},r.prototype.unshift=function(t){if(!(this.reference()instanceof Array))throw Error("baobab.Cursor.push: trying to push to non-array value.");return this.update(arguments.length>1?{$unshift:s.arrayOf(arguments)}:{$unshift:t})},r.prototype.merge=function(t){if(!a.Object(t))throw Error("baobab.Cursor.merge: trying to merge a non-object.");if(!a.Object(this.reference()))throw Error("baobab.Cursor.merge: trying to merge into a non-object.");this.update({$merge:t})},r.prototype.update=function(t){return this.tree.update(s.pathObject(this.solvedPath,t)),this},r.prototype.or=function(t){return new o("or",this,t)},r.prototype.and=function(t){return new o("and",this,t)},r.prototype.release=function(){this.tree.off("update",this.updateHandler),this.hash&&delete this.tree._cursors[this.hash],delete this.tree,delete this.path,delete this.solvedPath,this.kill()},r.prototype.toJSON=function(){return this.reference()},a.Cursor=function(t){return t instanceof r},e.exports=r},{"./combination.js":6,"./helpers.js":8,"./mixins.js":10,"./type.js":11,emmett:3}],8:[function(t,e){(function(r){function n(t){return Array.prototype.slice.call(t)}function o(t,e){var r,n={};for(r in t)n[r]=t[r];for(r in e)n[r]=e[r];return n}function i(t){var e=t.source,r="";return t.global&&(r+="g"),t.multiline&&(r+="m"),t.ignoreCase&&(r+="i"),t.sticky&&(r+="y"),t.unicode&&(r+="u"),new RegExp(e,r)}function s(t,e){if(!e||"object"!=typeof e||e instanceof Error||"ArrayBuffer"in r&&e instanceof ArrayBuffer)return e;if(b.Array(e)){if(t){var n,o,s=[];for(n=0,o=e.length;o>n;n++)s.push(v(e[n]));return s}return e.slice(0)}if(b.Date(e))return new Date(e.getTime());if(e instanceof RegExp)return i(e);if(b.Object(e)){var a,u={};e.constructor&&e.constructor!==Object&&(u=Object.create(e.constructor.prototype));for(a in e)e.hasOwnProperty(a)&&(u[a]=t?v(e[a]):e[a]);return u}return e}function a(t,e){return function(r){return e(t(r))}}function u(t,e){var r,n;for(r=0,n=t.length;n>r;r++)if(e(t[r]))return t[r]}function h(t,e){var r,n;for(r=0,n=t.length;n>r;r++)if(e(t[r]))return r;return-1}function c(t,e){var r,n=!0;if(!t)return!1;for(r in e)if(b.Object(e[r]))n=n&&c(t[r],e[r]);else if(b.Array(e[r]))n=n&&!!~e[r].indexOf(t[r]);else if(t[r]!==e[r])return!1;return n}function l(t,e){return u(t,function(t){return c(t,e)})}function f(t,e){return h(t,function(t){return c(t,e)})}function p(t,e){e=e||[];var r,n,o=t;for(r=0,n=e.length;n>r;r++){if(!o)return;if("function"==typeof e[r]){if(!b.Array(o))return;o=u(o,e[r])}else if("object"==typeof e[r]){if(!b.Array(o))return;o=l(o,e[r])}else o=o[e[r]]}return o}function d(t,e){var r,n,o,i=[],s=t;for(n=0,o=e.length;o>n;n++){if(!s)return null;if("function"==typeof e[n]){if(!b.Array(s))return;r=h(s,e[n]),i.push(r),s=s[r]}else if("object"==typeof e[n]){if(!b.Array(s))return;r=f(s,e[n]),i.push(r),s=s[r]}else i.push(e[n]),s=s[e[n]]||{}}return i}function y(t,e){var r,n=t.length,o={},i=o;for(n||(o=e),r=0;n>r;r++)i[t[r]]=r+1===n?e:{},i=i[t[r]];return o}function g(t,e){t.super_=e;var r=function(){};r.prototype=e.prototype,t.prototype=new r,t.prototype.constructor=t}var b=t("./type.js"),m=s.bind(null,!1),v=s.bind(null,!0);e.exports={arrayOf:n,deepClone:v,shallowClone:m,shallowMerge:o,compose:a,getIn:p,inherits:g,pathObject:y,solvePath:d}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./type.js":11}],9:[function(t,e){function r(t,e){return e in(t||{})}function n(t,e,n){return r(t,n)&&r(e,n)}function o(){var t,e,r,a,u={},h=arguments.length;for(r=h-1;r>=0;r--){if(arguments[r].$unset)delete u.$set,delete u.$apply,delete u.$merge,u.$unset=arguments[r].$unset;else{if(arguments[r].$set){delete u.$apply,delete u.$merge,delete u.$unset,u.$set=arguments[r].$set;continue}if(arguments[r].$merge){delete u.$set,delete u.$apply,delete u.$unset,u.$merge=arguments[r].$merge;continue}if(arguments[r].$apply){delete u.$set,delete u.$merge,delete u.$unset,u.$apply=arguments[r].$apply;continue}if(arguments[r].$chain){delete u.$set,delete u.$merge,delete u.$unset,u.$apply=u.$apply?i.compose(u.$apply,arguments[r].$chain):arguments[r].$chain;continue}}for(a in arguments[r])t=u[a],e=arguments[r][a],t&&s.Object(e)?n(t,e,"$push")?t.$push=s.Array(t.$push)?t.$push.concat(e.$push):[t.$push].concat(e.$push):n(t,e,"$unshift")?t.$unshift=s.Array(e.$unshift)?e.$unshift.concat(t.$unshift):[e.$unshift].concat(t.$unshift):u[a]=o(e,t):u[a]=e}return u}var i=t("./helpers.js"),s=t("./type.js");e.exports=o},{"./helpers.js":8,"./type.js":11}],10:[function(t,e){var r=t("./combination.js"),n=t("./type.js");e.exports={baobab:function(t){return{mixins:[{getInitialState:function(){if(this.tree=t,!this.cursor&&!this.cursors)return{};if(this.cursor&&this.cursors)throw Error("baobab.mixin: you cannot have both `component.cursor` and `component.cursors`. Please make up your mind.");if(this.__type=null,this.__updateHandler=function(){this.setState(this.__getCursorData())}.bind(this),this.cursor){if(!n.MixinCursor(this.cursor))throw Error("baobab.mixin.cursor: invalid data (cursor, string, array or function).");n.Function(this.cursor)&&(this.cursor=this.cursor()),n.Cursor(this.cursor)||(this.cursor=t.select(this.cursor)),this.__getCursorData=function(){return{cursor:this.cursor.get()}}.bind(this),this.__type="single"}else if(this.cursors){if(!n.MixinCursors(this.cursors))throw Error("baobab.mixin.cursor: invalid data (object, array or function).");if(n.Function(this.cursors)&&(this.cursors=this.cursors()),n.Array(this.cursors))this.cursors=this.cursors.map(function(e){return n.Cursor(e)?e:t.select(e)}),this.__getCursorData=function(){return{cursors:this.cursors.map(function(t){return t.get()})}}.bind(this),this.__type="array";else{for(var e in this.cursors)n.Cursor(this.cursors[e])||(this.cursors[e]=t.select(this.cursors[e]));this.__getCursorData=function(){var t={};for(e in this.cursors)t[e]=this.cursors[e].get();return{cursors:t}}.bind(this),this.__type="object"}}return this.__getCursorData()},componentDidMount:function(){"single"===this.__type?(this.__combination=new r("or",[this.cursor]),this.__combination.on("update",this.__updateHandler)):"array"===this.__type?(this.__combination=new r("or",this.cursors),this.__combination.on("update",this.__updateHandler)):"object"===this.__type&&(this.__combination=new r("or",Object.keys(this.cursors).map(function(t){return this.cursors[t]},this)),this.__combination.on("update",this.__updateHandler))},componentWillUnmount:function(){this.__combination&&this.__combination.release()}}].concat(t.options.mixins)}},cursor:function(t){return{mixins:[{getInitialState:function(){return this.cursor=t,this.__updateHandler=function(){this.setState({cursor:this.cursor.get()})}.bind(this),{cursor:this.cursor.get()}},componentDidMount:function(){this.cursor.on("update",this.__updateHandler)},componentWillUnmount:function(){this.cursor.off("update",this.__updateHandler)}}].concat(t.tree.options.mixins)}}}},{"./combination.js":6,"./type.js":11}],11:[function(t,e){var r=function(t){return Array.isArray(t)?"array":"object"==typeof t&&null!==t?"object":"string"==typeof t?"string":"number"==typeof t?"number":"boolean"==typeof t?"boolean":"function"==typeof t?"function":null===t?"null":void 0===t?"undefined":t instanceof Date?"date":"invalid"};r.Array=function(t){return Array.isArray(t)},r.Object=function(t){return!Array.isArray(t)&&"object"==typeof t&&null!==t},r.String=function(t){return"string"==typeof t},r.Number=function(t){return"number"==typeof t},r.Boolean=function(t){return"boolean"==typeof t},r.Function=function(t){return"function"==typeof t},r.Primitive=function(t){return"string"==typeof t||"number"==typeof t||"boolean"==typeof t},r.Date=function(t){return t instanceof Date},r.Step=function(t){var e=r(t),n=["null","undefined","invalid","date"];return-1===n.indexOf(e)},r.Path=function(t){var e=["object","string","number","function","undefined"];if(!r.Array(t))return e.indexOf(r(t))>=0;for(var n=0;n=0||r.Cursor(t)},r.MixinCursors=function(t){var e=["array","object","function"];return e.indexOf(r(t))>=0},r.ComplexPath=function(t){for(var e=["object","function"],n=0;n=0)return!0;return!1},e.exports=r},{}],12:[function(t,e){function r(t,e){var r=new Error("baobab.update: "+e+" at path /"+t.toString());return r.path=t,r}function n(t,e,n){n=n||{shiftReferences:!1};var a={};return function u(t,e,h,c){h=h||[];var l,f,p,d,y=h.join("λ");for(p in e)if(s[p])switch(d=e[p],a[y]=!0,p){case"$push":if(!i.Array(t))throw r(h,"using command $push to a non array");i.Array(d)?t.push.apply(t,d):t.push(d);break;case"$unshift":if(!i.Array(t))throw r(h,"using command $unshift to a non array");i.Array(d)?t.unshift.apply(t,d):t.unshift(d)}else if(f=y?y+"λ"+p:p,"$unset"in(e[p]||{}))a[f]=!0,i.Array(t)?n.shiftReferences?c[h[h.length-1]]=t.slice(0,+p).concat(t.slice(+p+1)):t.splice(p,1):delete t[p];else if("$set"in(e[p]||{}))d=e[p].$set,a[f]=!0,t[p]=d;else if("$apply"in(e[p]||{})||"$chain"in(e[p]||{})){if(l=e[p].$apply||e[p].$chain,"function"!=typeof l)throw r(h.concat(p),"using command $apply with a non function");a[f]=!0,t[p]=l.call(null,t[p])}else if("$merge"in(e[p]||{})){if(d=e[p].$merge,!i.Object(t[p]))throw r(h.concat(p),"using command $merge on a non-object");a[f]=!0,t[p]=o.shallowMerge(t[p],d)}else if(n.shiftReferences&&("$push"in(e[p]||{})||"$unshift"in(e[p]||{}))){if("$push"in(e[p]||{})){if(d=e[p].$push,!i.Array(t[p]))throw r(h.concat(p),"using command $push to a non array");t[p]=t[p].concat(d)}if("$unshift"in(e[p]||{})){if(d=e[p].$unshift,!i.Array(t[p]))throw r(h.concat(p),"using command $unshift to a non array");t[p]=(d instanceof Array?d:[d]).concat(t[p])}a[f]=!0}else"undefined"==typeof t[p]&&(t[p]={}),n.shiftReferences&&(t[p]=o.shallowClone(t[p])),u(t[p],e[p],h.concat(p),t)}(t,e),Object.keys(a).map(function(t){return t.split("λ")})}var o=t("./helpers.js"),i=t("./type.js"),s={};["$set","$push","$unshift","$apply","$merge"].forEach(function(t){s[t]=!0}),e.exports=n},{"./helpers.js":8,"./type.js":11}]},{},[1])(1)}); \ No newline at end of file +/* baobab.js - Version: 1.0.0 - Author: Yomguithereal (Guillaume Plique) */ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Baobab=t()}}(function(){var t;return function e(t,r,n){function o(s,a){if(!r[s]){if(!t[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(i)return i(s,!0);var h=new Error("Cannot find module '"+s+"'");throw h.code="MODULE_NOT_FOUND",h}var c=r[s]={exports:{}};t[s][0].call(c.exports,function(e){var r=t[s][1][e];return o(r?r:e)},c,c.exports,e,t,r,n)}return r[s].exports}for(var i="function"==typeof require&&require,s=0;sn;n++)t[n].fn!==e&&o.push(t[n]);return o}var s={once:"boolean",scope:"object"},a=0,u=function(){this._enabled=!0,this.unbindAll()};u.prototype.unbindAll=function(){return this._handlers={},this._handlersAll=[],this._handlersComplex=[],this},u.prototype.on=function(t,e,r){var n,i,u,h,c,l,f;if(o(t)){for(h in t)this.on(h,t[h],e);return this}for("function"==typeof t&&(r=e,e=t,t=null),c=[].concat(t),n=0,i=c.length;i>n;n++){if(h=c[n],f={order:a++,fn:e},"string"==typeof h)this._handlers[h]||(this._handlers[h]=[]),l=this._handlers[h];else if(h instanceof RegExp)l=this._handlersComplex,f.pattern=h;else{if(null!==h)throw Error("Emitter.on: invalid event.");l=this._handlersAll}for(u in r||{})s[u]&&(f[u]=r[u]);f.once&&(f.parent=l),l.push(f)}return this},u.prototype.once=function(){var t=Array.prototype.slice.call(arguments),r=t.length-1;return o(t[r])&&t.length>1?t[r]=e(t[r],{once:!0}):t.push({once:!0}),this.on.apply(this,t)},u.prototype.off=function(t,e){var r,n,s,a;if(1===arguments.length&&"function"==typeof t){e=arguments[0];for(s in this._handlers)this._handlers[s]=i(this._handlers[s],e),0===this._handlers[s].length&&delete this._handlers[s];this._handlersAll=i(this._handlersAll,e),this._handlersComplex=i(this._handlersComplex,e)}else if(1===arguments.length&&"string"==typeof t)delete this._handlers[t];else if(2===arguments.length){var u=[].concat(t);for(r=0,n=u.length;n>r;r++)a=u[r],this._handlers[a]=i(this._handlers[a],e),0===(this._handlers[a]||[]).length&&delete this._handlers[a]}else if(o(t))for(s in t)this.off(s,t[s]);return this},u.prototype.listeners=function(t){var e,r,n,o=this._handlersAll||[],i=!1;if(!t)throw Error("Emitter.listeners: no event provided.");for(o=o.concat(this._handlers[t]||[]),r=0,n=this._handlersComplex.length;n>r;r++)e=this._handlersComplex[r],~t.search(e.pattern)&&(i=!0,o.push(e));return this._handlersAll.length||i?o.sort(function(t,e){return t.order-e.order}):o.slice(0)},u.prototype.emit=function(t,e){if(!this._enabled)return this;if(o(t)){for(var r in t)this.emit(r,t[r]);return this}var n,i,s,a,u,h,c,l=[].concat(t),f=[];for(a=0,h=l.length;h>a;a++){for(i=this.listeners(l[a]),u=0,c=i.length;c>u;u++)s=i[u],n={type:l[a],target:this},arguments.length>1&&(n.data=e),s.fn.call("scope"in s?s.scope:this,n),s.once&&f.push(s);for(u=f.length-1;u>=0;u--)f[u].parent.splice(f[u].parent.indexOf(f[u]),1)}return this},u.prototype.kill=function(){this.unbindAll(),this._handlers=null,this._handlersAll=null,this._handlersComplex=null,this._enabled=!1},u.prototype.disable=function(){return this._enabled=!1,this},u.prototype.enable=function(){return this._enabled=!0,this},u.version="3.0.0","undefined"!=typeof n?("undefined"!=typeof r&&r.exports&&(n=r.exports=u),n.Emitter=u):"function"==typeof t&&t.amd?t("emmett",[],function(){return u}):this.Emitter=u}).call(this)},{}],4:[function(t,e,r){function n(t){return t+"$"+(new Date).getTime()+(""+Math.random()).replace("0.","")}function o(t,e){function r(t){this[t]=function(){var e=this.root[t].apply(this.root,arguments);return e instanceof i?this:e}}if(arguments.length<1&&(t={}),!(this instanceof o))return new o(t,e);if(!f.Object(t)&&!f.Array(t))throw Error("Baobab: invalid data.");if(s.call(this),this.options=u.shallowMerge(l,e),this._transaction={},this._future=void 0,this._cursors={},this._identity="[object Baobab]",this.data=u.deepClone(t),this.root=this.select([]),this.facets={},["get","set","unset","update"].forEach(r.bind(this)),!f.Object(this.options.facets))throw Error("Baobab: invalid facets.");for(var n in this.options.facets)this.addFacet(n,this.options.facets[n])}var i=t("./cursor.js"),s=t("emmett"),a=t("./facet.js"),u=t("./helpers.js"),h=t("./update.js"),c=t("./merge.js"),l=t("../defaults.js"),f=t("./type.js");u.inherits(o,s),o.prototype.addFacet=function(t,e){return this.facets[t]=this.createFacet(e),this},o.prototype.createFacet=function(t){return new a(this,t)},o.prototype.select=function(t){if(!t)throw Error("Baobab.select: invalid path.");if(arguments.length>1&&(t=u.arrayOf(arguments)),!f.Path(t))throw Error("Baobab.select: invalid path.");t=[].concat(t);var e,r=f.ComplexPath(t);r&&(e=u.solvePath(this.data,t,this));var o=t.map(function(t){return f.Function(t)?n("fn"):f.Object(t)?n("ob"):t}).join("|λ|");if(this._cursors[o])return this._cursors[o];var s=new i(this,t,e,o);return this._cursors[o]=s,s},o.prototype.stack=function(t){var e=this;if(!f.Object(t))throw Error("Baobab.update: wrong specification.");return this._transaction=c(t,this._transaction),this.options.autoCommit?this.options.asynchronous?(this._future||(this._future=setTimeout(e.commit.bind(e,null),0)),this):this.commit():this},o.prototype.commit=function(){var t=h(this.data,this._transaction,this.options),e=this.data;this._transaction={},this._future&&(this._future=clearTimeout(this._future));var r=this.options.validate,n=this.options.validationBehavior;if("function"==typeof r){var o=r.call(this,e,t.data,t.log);if(o instanceof Error&&(this.emit("invalid",{error:o}),"rollback"===n))return this}return this.data=t.data,this.emit("update",{log:t.log,previousState:e}),this},o.prototype.release=function(){var t;delete this.data,delete this._transaction;for(t in this._cursors)this._cursors[t].release();delete this._cursors;for(t in this.facets)this.facets[t].release();delete this.facets,this.kill()},o.prototype.toJSON=function(){return this.get()},o.prototype.toString=function(){return this._identity},e.exports=o},{"../defaults.js":2,"./cursor.js":5,"./facet.js":6,"./helpers.js":7,"./merge.js":8,"./type.js":9,"./update.js":10,emmett:3}],5:[function(t,e,r){function n(t,e,r,n){function o(t){if(i.recording&&!i.undoing){var e=a.getIn(t,i.solvedPath,i.tree),r=a.deepClone(e);i.archive.add(r)}return i.undoing=!1,i.emit("update")}var i=this;s.call(this),e=e||[],this.tree=t,this.path=e,this.hash=n,this.archive=null,this.recording=!1,this.undoing=!1,this._identity="[object Cursor]",this.complexPath=!!r,this.solvedPath=this.complexPath?r:this.path,this.relevant=void 0!==this.get(),this.updateHandler=function(t){var e=t.data.log,r=t.data.previousState,n=!1;if(i.complexPath&&(i.solvedPath=a.solvePath(i.tree.data,i.path,i.tree)),!i.path.length)return o(r);i.solvedPath&&(n=a.solveUpdate(e,[i.solvedPath]));var s=void 0!==i.get();i.relevant?s&&n?o(r):s||(i.emit("irrelevant"),i.relevant=!1):s&&n&&(i.emit("relevant"),o(r),i.relevant=!0)};var u=!1;this._lazyBind=function(){u||(u=!0,i.tree.on("update",i.updateHandler))},this.on=a.before(this._lazyBind,this.on.bind(this)),this.once=a.before(this._lazyBind,this.once.bind(this)),this.complexPath&&this._lazyBind()}function o(t,e,r,n){if(arguments.length>5)throw Error("baobab.Cursor."+t+": too many arguments.");if(arguments.length<4&&(n=r,r=[]),r=r||[],"splice"===t&&!u.Splicer(n)){if(!u.Array(n))throw Error("baobab.Cursor.splice: incorrect value.");n=[n]}if(e&&!e(n))throw Error("baobab.Cursor."+t+": incorrect value.");var o=[].concat(r),i=a.solvePath(this.get(),o,this.tree);if(!i)throw Error("baobab.Cursor."+t+": could not solve dynamic path.");var s={};s["$"+t]=n;var h=a.pathObject(i,s);return h}function i(t,e){n.prototype[t]=function(){var r=o.bind(this,t,e).apply(this,arguments);return this.update(r)}}var s=t("emmett"),a=t("./helpers.js"),u=(t("../defaults.js"),t("./type.js"));a.inherits(n,s),n.prototype.isRoot=function(){return!this.path.length},n.prototype.isLeaf=function(){return u.Primitive(this.get())},n.prototype.isBranch=function(){return!this.isLeaf()&&!this.isRoot()},n.prototype.root=function(){return this.tree.root()},n.prototype.select=function(t){if(arguments.length>1&&(t=a.arrayOf(arguments)),!u.Path(t))throw Error("baobab.Cursor.select: invalid path.");return this.tree.select(this.path.concat(t))},n.prototype.up=function(){return this.solvedPath&&this.solvedPath.length?this.tree.select(this.path.slice(0,-1)):null},n.prototype.left=function(){var t=+this.solvedPath[this.solvedPath.length-1];if(isNaN(t))throw Error("baobab.Cursor.left: cannot go left on a non-list type.");return t?this.tree.select(this.solvedPath.slice(0,-1).concat(t-1)):null},n.prototype.leftmost=function(){var t=+this.solvedPath[this.solvedPath.length-1];if(isNaN(t))throw Error("baobab.Cursor.leftmost: cannot go left on a non-list type.");return this.tree.select(this.solvedPath.slice(0,-1).concat(0))},n.prototype.right=function(){var t=+this.solvedPath[this.solvedPath.length-1];if(isNaN(t))throw Error("baobab.Cursor.right: cannot go right on a non-list type.");return t+1===this.up().get().length?null:this.tree.select(this.solvedPath.slice(0,-1).concat(t+1))},n.prototype.rightmost=function(){var t=+this.solvedPath[this.solvedPath.length-1];if(isNaN(t))throw Error("baobab.Cursor.right: cannot go right on a non-list type.");var e=this.up().get();return this.tree.select(this.solvedPath.slice(0,-1).concat(e.length-1))},n.prototype.down=function(){+this.solvedPath[this.solvedPath.length-1];return this.get()instanceof Array?this.tree.select(this.solvedPath.concat(0)):null},n.prototype.get=function(t){arguments.length>1&&(t=a.arrayOf(arguments));var e=this.solvedPath.concat([].concat(t||0===t?t:[]));return a.getIn(this.tree.data,e,this.tree)},i("set"),i("apply",u.Function),i("chain",u.Function),i("push"),i("unshift"),i("merge",u.Object),i("splice"),n.prototype.unset=function(t){if(void 0===t&&this.isRoot())throw Error("baobab.Cursor.unset: cannot remove root node.");var e=o.bind(this,"unset",null).apply(this,[t,!0]);return this.update(e)},n.prototype.update=function(t,e){if(arguments.length<2)return this.tree.stack(a.pathObject(this.solvedPath,t)),this;var r=[].concat(t),n=a.solvePath(this.get(),r,this.tree);if(!n)throw Error("baobab.Cursor.update: could not solve dynamic path.");return this.tree.stack(a.pathObject(this.solvedPath.concat(n),e)),this},n.prototype.startRecording=function(t){if(t=t||5,1>t)throw Error("baobab.Cursor.startRecording: invalid maximum number of records.");return this.archive?this:(this._lazyBind(),this.archive=a.archive(t),this.recording=!0,this)},n.prototype.stopRecording=function(){return this.recording=!1,this},n.prototype.undo=function(t){if(t=t||1,!this.recording)throw Error("baobab.Cursor.undo: cursor is not recording.");if(!u.PositiveInteger(t))throw Error("baobab.Cursor.undo: expecting a positive integer.");var e=this.archive.back(t);if(!e)throw Error("boabab.Cursor.undo: cannot find a relevant record ("+t+" back).");return this.undoing=!0,this.set(e)},n.prototype.hasHistory=function(){return!(!this.archive||!this.archive.get().length)},n.prototype.getHistory=function(){return this.archive?this.archive.get():[]},n.prototype.clearHistory=function(){return this.archive=null,this},n.prototype.release=function(){this.tree.off("update",this.updateHandler),this.hash&&delete this.tree._cursors[this.hash],delete this.tree,delete this.path,delete this.solvedPath,delete this.archive,this.kill()},n.prototype.toJSON=function(){return this.get()},n.prototype.toString=function(){return this._identity},e.exports=n},{"../defaults.js":2,"./helpers.js":7,"./type.js":9,emmett:3}],6:[function(t,e,r){function n(t,e,r){function u(e,o,s,u){if(e||c){l=!1;var f=o;if(e&&(f=o.call(r)),!u(f))throw Error("baobab.Facet: incorrect "+s+" mapping.");h[s]={},Object.keys(f).forEach(function(e){if("cursors"===s){if(f[e]instanceof i)return void(h.cursors[e]=f[e]);if(a.Path(f[e]))return void(h.cursors[e]=t.select(f[e]))}else{if(f[e]instanceof n)return void(h.facets[e]=f[e]);if("string"==typeof f[e]){if(h.facets[e]=t.facets[f[e]],!h.facets)throw Error('baobab.Facet: unkown "'+f[e]+'" facet in facets mapping.');return}}throw Error("baobab.Facet: invalid value returned by function in "+s+" mapping.")})}}var h=this,c=!0,l=!1,f=e.get;o.call(this),this.tree=t,this.cursors={},this.facets={};var p=e.cursors,d=e.facets,y="function"==typeof e.cursors,v="function"==typeof e.facets;this.refresh=function(){p&&u(y,p,"cursors",a.FacetCursors),d&&u(v,d,"facets",a.FacetFacets)},this.get=function(){if(l)return e;var t,e={};for(t in h.facets)e[t]=h.facets[t].get();for(t in h.cursors)e[t]=h.cursors[t].get();return e="function"==typeof f?f.call(null,e):e,l=!0,e},this.updateHandler=function(t){var e=Object.keys(h.cursors).map(function(t){return h.cursors[t].solvedPath});s.solveUpdate(t.data.log,e)&&(l=!1,h.emit("update"))},this.refresh(),this.tree.on("update",this.updateHandler),c=!1}var o=t("emmett"),i=t("./cursor.js"),s=t("./helpers.js"),a=t("./type.js");s.inherits(n,o),n.prototype.release=function(){this.tree.off("update",this.updateHandler),this.tree=null,this.cursors=null,this.facets=null,this.kill()},e.exports=n},{"./cursor.js":5,"./helpers.js":7,"./type.js":9,emmett:3}],7:[function(t,e,r){(function(r){function n(t){return Array.prototype.slice.call(t)}function o(t,e){return function(){t(),e.apply(null,arguments)}}function i(t,e,r){var o=n(arguments).slice(3);return e=+e,r=+r,t.slice(0,e).concat(t.slice(e+r).concat(o))}function s(t,e){var r,n={};for(r in t)n[r]=t[r];for(r in e)n[r]=e[r];return n}function a(t){var e=t.source,r="";return t.global&&(r+="g"),t.multiline&&(r+="m"),t.ignoreCase&&(r+="i"),t.sticky&&(r+="y"),t.unicode&&(r+="u"),new RegExp(e,r)}function u(t,e){if(!e||"object"!=typeof e||e instanceof Error||"ArrayBuffer"in r&&e instanceof ArrayBuffer)return e;if(w.Array(e)){if(t){var n,o,i=[];for(n=0,o=e.length;o>n;n++)i.push(P(e[n]));return i}return e.slice(0)}if(w.Date(e))return new Date(e.getTime());if(e instanceof RegExp)return a(e);if(w.Object(e)){var s,u={};e.constructor&&e.constructor!==Object&&(u=Object.create(e.constructor.prototype));for(s in e)e.hasOwnProperty(s)&&(u[s]=t?P(e[s]):e[s]);return u}return e}function h(t,e){return function(r){return e(t(r))}}function c(t,e){var r,n;for(r=0,n=t.length;n>r;r++)if(e(t[r]))return t[r]}function l(t,e){var r,n;for(r=0,n=t.length;n>r;r++)if(e(t[r]))return r;return-1}function f(t,e){var r,n=!0;if(!t)return!1;for(r in e)if(w.Object(e[r]))n=n&&f(t[r],e[r]);else if(w.Array(e[r]))n=n&&!!~e[r].indexOf(t[r]);else if(t[r]!==e[r])return!1;return n}function d(t,e){return c(t,function(t){return f(t,e)})}function y(t,e){return l(t,function(t){return f(t,e)})}function v(t,e,r){e=e||[];var n,o,i,s=t;for(o=0,i=e.length;i>o;o++){if(!s)return;if("function"==typeof e[o]){if(!w.Array(s))return;s=c(s,e[o])}else if("object"==typeof e[o])if(r&&"$cursor"in e[o]){if(!w.Path(e[o].$cursor))throw Error("baobab.getIn: $cursor path must be an array.");n=r.get(e[o].$cursor),s=s[n]}else{if(!w.Array(s))return;s=d(s,e[o])}else s=s[e[o]]}return s}function g(t,e,r){var n,o,i,s=[],a=t;for(o=0,i=e.length;i>o;o++){if(!a)return null;if("function"==typeof e[o]){if(!w.Array(a))return;n=l(a,e[o]),s.push(n),a=a[n]}else if("object"==typeof e[o])if(r&&"$cursor"in e[o]){if(!w.Path(e[o].$cursor))throw Error("baobab.getIn: $cursor path must be an array.");p=r.get(e[o].$cursor),s.push(p),a=a[p]}else{if(!w.Array(a))return;n=y(a,e[o]),s.push(n),a=a[n]}else s.push(e[o]),a=a[e[o]]||{}}return s}function b(t,e){var r,n,o,i,s,a,u,h,c;for(r=0,i=e.length;i>r;r++)for(u=e[r],n=0,s=t.length;s>n;n++)for(h=t[n],o=0,a=h.length;a>o&&(c=h[o],c==u[o]);o++)if(o+1===a||o+1===u.length)return!0;return!1}function m(t,e){var r,n=t.length,o={},i=o;for(n||(o=e),r=0;n>r;r++)i[t[r]]=r+1===n?e:{},i=i[t[r]];return o}function j(t,e){t.super_=e;var r=function(){};r.prototype=e.prototype,t.prototype=new r,t.prototype.constructor=t}function _(t){var e=[];return{add:function(r){e.unshift(r),e.length>t&&(e.length=t)},back:function(t){var r=e[t-1];return r&&(e=e.slice(t)),r},get:function(){return e}}}var w=t("./type.js"),$=u.bind(null,!1),P=u.bind(null,!0);e.exports={archive:_,arrayOf:n,before:o,deepClone:P,shallowClone:$,shallowMerge:s,compose:h,getIn:v,inherits:j,pathObject:m,solvePath:g,solveUpdate:b,splice:i}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./type.js":9}],8:[function(t,e,r){function n(t,e,r){a.forEach(function(e){r!==e&&delete t[e]}),t[r]=e[r]}function o(t,e){var r,u=i.shallowClone(e||{});a.forEach(function(e){t[e]&&n(u,t,e)}),t.$chain&&(a.slice(0,-1).forEach(function(t){delete u[t]}),u.$apply=u.$apply?i.compose(u.$apply,t.$chain):t.$chain),(t.$splice||u.$splice)&&(u.$splice=[].concat(u.$splice||[]).concat(t.$splice||[])),(t.$push||u.$push)&&(u.$push=[].concat(u.$push||[]).concat(t.$push||[])),(t.$unshift||u.$unshift)&&(u.$unshift=[].concat(t.$unshift||[]).concat(u.$unshift||[]));for(r in t)s.Object(t[r])?u[r]=o(t[r],u[r]):"$"!==r[0]&&(u[r]=t[r]);return u}var i=t("./helpers.js"),s=t("./type.js"),a=["$unset","$set","$merge","$apply"];e.exports=o},{"./helpers.js":7,"./type.js":9}],9:[function(t,e,r){function n(t,e){return e.some(function(e){return o[e](t)})}var o={};o.Array=function(t){return Array.isArray(t)},o.Object=function(t){return t&&"object"==typeof t&&!Array.isArray(t)&&!(t instanceof Function)},o.String=function(t){return"string"==typeof t},o.Number=function(t){return"number"==typeof t},o.PositiveInteger=function(t){return"number"==typeof t&&t>0&&t%1===0},o.Function=function(t){return"function"==typeof t},o.Primitive=function(t){return"string"==typeof t||"number"==typeof t||"boolean"==typeof t},o.Date=function(t){return t instanceof Date},o.NonScalar=function(t){return o.Object(t)||o.Array(t)},o.Splicer=function(t){return o.Array(t)&&t.every(o.Array)},o.Path=function(t){var e=["String","Number","Function","Object"];return o.Array(t)?t.every(function(t){return n(t,e)}):n(t,e)},o.ComplexPath=function(t){return t.some(function(t){return n(t,["Object","Function"])})},o.FacetCursors=function(e){return o.Object(e)?Object.keys(e).every(function(r){var n=e[r];return o.Path(n)||n instanceof t("./cursor.js")}):!1},o.FacetFacets=function(e){return o.Object(e)?Object.keys(e).every(function(r){var n=e[r];return"string"==typeof n||n instanceof t("./facet.js")}):!1},e.exports=o},{"./cursor.js":5,"./facet.js":6}],10:[function(t,e,r){function n(t,e){var r=new Error("baobab.update: "+e+" at path /"+t.slice(1).join("/"));return r.path=t,r}var o=t("./helpers.js"),i=t("./type.js");e.exports=function(t,e,r){if(r=r||{},!i.Object(t)&&!i.Array(t))throw Error("baobab.update: invalid target.");var s={};t={root:o.shallowClone(t)};var a=function(t,e,r,u){r=r||["root"];var h,c,l,f=r.join("|λ|"),p=r[r.length-1],d=Object.keys(e).some(function(t){return!!~["$set","$push","$unshift","$splice","$unset","$merge","$apply"].indexOf(t)});if(d){s[f]=!0;for(c in e){if("$unset"===c){var y=r[r.length-2];if(!i.Object(u[y]))throw n(r.slice(0,-1),"using command $unset on a non-object");u[y]=o.shallowClone(t),delete u[y][p];break}if("$set"===c){l=e.$set,t[p]=l;break}if("$apply"===c){if(h=e.$apply,"function"!=typeof h)throw n(r,"using command $apply with a non function");t[p]=h.call(null,t[p]);break}if("$merge"===c){if(l=e.$merge,!i.Object(t[p])||!i.Object(l))throw n(r,"using command $merge with a non object");t[p]=o.shallowMerge(t[p],l);break}if("$splice"===c){if(l=e.$splice,!i.Array(t[p]))throw n(r,"using command $push to a non array");l.forEach(function(e){t[p]=o.splice.apply(null,[t[p]].concat(e))})}if("$push"===c){if(l=e.$push,!i.Array(t[p]))throw n(r,"using command $push to a non array");t[p]=t[p].concat(l)}if("$unshift"===c){if(l=e.$unshift,!i.Array(t[p]))throw n(r,"using command $unshift to a non array");t[p]=[].concat(l).concat(t[p])}}}else for(c in e)t[p][c]="undefined"==typeof t[p][c]?{}:o.shallowClone(t[p][c]),a(t[p],e[c],r.concat(c),t)};return a(t,e),{data:t.root,log:Object.keys(s).map(function(t){return t.split("|λ|").slice(1)})}}},{"./helpers.js":7,"./type.js":9}]},{},[1])(1)}); \ No newline at end of file diff --git a/defaults.js b/defaults.js index 85ef97b..08a8454 100644 --- a/defaults.js +++ b/defaults.js @@ -11,27 +11,12 @@ module.exports = { // Should the transactions be handled asynchronously? asynchronous: true, - // Should the tree clone data when giving it back to the user? - clone: false, - - // Which cloning function should the tree use? - cloningFunction: null, - - // Should cursors be singletons? - cursorSingletons: true, - - // Maximum records in the tree's history - maxHistory: 0, - - // Collection of react mixins to merge with the tree's ones - mixins: [], - - // Should the tree shift its internal reference when applying mutations? - shiftReferences: false, - - // Custom typology object to use along with the validation utilities - typology: null, + // Facets registration + facets: {}, // Validation specifications - validate: null + validate: null, + + // Validation behaviour 'rollback' or 'notify' + validationBehavior: 'rollback' }; diff --git a/index.js b/index.js index c8eff74..dc1c56b 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ var Baobab = require('./src/baobab.js'), // Non-writable version Object.defineProperty(Baobab, 'version', { - value: '0.4.4' + value: '1.0.0' }); // Exposing helpers diff --git a/package.json b/package.json index 1415ce0..1b710bf 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,10 @@ { "name": "baobab", - "version": "0.4.4", + "version": "1.0.0", "description": "JavaScript data tree with cursors.", "main": "index.js", "dependencies": { - "emmett": "^2.1.2", - "typology": "^0.3.1" + "emmett": "^3.0.0" }, "devDependencies": { "async": "~0.9.0", @@ -16,10 +15,8 @@ "gulp-mocha": "^2.0.0", "gulp-replace": "^0.5.3", "gulp-uglify": "^1.0.2", - "jsdom": "^3.1.0", - "lodash.clonedeep": "^3.0.0", + "lodash": "^3.6.0", "mocha": "^2.0.1", - "react": "^0.13.0", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0" }, diff --git a/src/baobab.js b/src/baobab.js index 0269103..a712aea 100644 --- a/src/baobab.js +++ b/src/baobab.js @@ -6,11 +6,10 @@ */ var Cursor = require('./cursor.js'), EventEmitter = require('emmett'), - Typology = require('typology'), + Facet = require('./facet.js'), helpers = require('./helpers.js'), update = require('./update.js'), merge = require('./merge.js'), - mixins = require('./mixins.js'), defaults = require('../defaults.js'), type = require('./type.js'); @@ -38,128 +37,50 @@ function Baobab(initialData, opts) { // Merging defaults this.options = helpers.shallowMerge(defaults, opts); - this._cloner = this.options.cloningFunction || helpers.deepClone; // Privates this._transaction = {}; this._future = undefined; - this._history = []; this._cursors = {}; + this._identity = '[object Baobab]'; - // Internal typology - this.typology = this.options.typology ? - (this.options.typology instanceof Typology ? - this.options.typology : - new Typology(this.options.typology)) : - new Typology(); - - // Internal validation - this.validate = this.options.validate || null; + // Properties + this.data = helpers.deepClone(initialData); + this.root = this.select([]); + this.facets = {}; + + // Boostrapping root cursor's methods + function bootstrap(name) { + this[name] = function() { + var r = this.root[name].apply(this.root, arguments); + return r instanceof Cursor ? this : r; + }; + } - if (this.validate) - try { - this.typology.check(initialData, this.validate, true); - } - catch (e) { - e.message = '/' + e.path.join('/') + ': ' + e.message; - throw e; - } + ['get', 'set', 'unset', 'update'].forEach(bootstrap.bind(this)); - // Properties - this.data = this._cloner(initialData); + // Facets + if (!type.Object(this.options.facets)) + throw Error('Baobab: invalid facets.'); - // Mixin - this.mixin = mixins.baobab(this); + for (var k in this.options.facets) + this.addFacet(k, this.options.facets[k]); } helpers.inherits(Baobab, EventEmitter); -/** - * Private prototype - */ -Baobab.prototype._archive = function() { - if (this.options.maxHistory <= 0) - return; - - var record = { - data: this._cloner(this.data) - }; - - // Replacing - if (this._history.length === this.options.maxHistory) { - this._history.pop(); - } - this._history.unshift(record); - - return record; -}; - /** * Prototype */ -Baobab.prototype.commit = function(referenceRecord) { - var self = this, - log; - - if (referenceRecord) { - - // Override - this.data = referenceRecord.data; - log = referenceRecord.log; - } - else { - - // Shifting root reference - if (this.options.shiftReferences) - this.data = helpers.shallowClone(this.data); - - // Applying modification (mutation) - var record = this._archive(); - log = update(this.data, this._transaction, this.options); - - if (record) - record.log = log; - } - - 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}); - } - - // Resetting - this._transaction = {}; - - if (this._future) - this._future = clearTimeout(this._future); - - // Baobab-level update event - this.emit('update', { - log: log - }); - +Baobab.prototype.addFacet = function(name, definition) { + this.facets[name] = this.createFacet(definition); return this; }; +Baobab.prototype.createFacet = function(definition) { + return new Facet(this, definition); +}; + Baobab.prototype.select = function(path) { if (!path) throw Error('Baobab.select: invalid path.'); @@ -171,7 +92,7 @@ Baobab.prototype.select = function(path) { throw Error('Baobab.select: invalid path.'); // Casting to array - path = !type.Array(path) ? [path] : path; + path = [].concat(path); // Complex path? var complex = type.ComplexPath(path); @@ -179,94 +100,29 @@ Baobab.prototype.select = function(path) { var solvedPath; if (complex) - solvedPath = helpers.solvePath(this.data, path); + solvedPath = helpers.solvePath(this.data, path, this); // Registering a new cursor or giving the already existing one for path - if (!this.options.cursorSingletons) { - return new Cursor(this, path); + var hash = path.map(function(step) { + if (type.Function(step)) + return complexHash('fn'); + else if (type.Object(step)) + return complexHash('ob'); + else + return step; + }).join('|λ|'); + + if (!this._cursors[hash]) { + var cursor = new Cursor(this, path, solvedPath, hash); + this._cursors[hash] = cursor; + return cursor; } else { - var hash = path.map(function(step) { - if (type.Function(step)) - return complexHash('fn'); - else if (type.Object(step)) - return complexHash('ob'); - else - return step; - }).join('λ'); - - if (!this._cursors[hash]) { - var cursor = new Cursor(this, path, solvedPath, hash); - this._cursors[hash] = cursor; - return cursor; - } - else { - return this._cursors[hash]; - } + return this._cursors[hash]; } }; -Baobab.prototype.root = function() { - return this.select([]); -}; - -Baobab.prototype.reference = function(path) { - var data; - - if (arguments.length > 1) - path = helpers.arrayOf(arguments); - - if (!type.Path(path)) - throw Error('Baobab.get: invalid path.'); - - return helpers.getIn( - this.data, type.String(path) || type.Number(path) ? [path] : path - ); -}; - -Baobab.prototype.get = function() { - var ref = this.reference.apply(this, arguments); - - return this.options.clone ? this._cloner(ref) : ref; -}; - -Baobab.prototype.clone = function(path) { - return this._cloner(this.reference.apply(this, arguments)); -}; - -Baobab.prototype.set = function(key, val) { - - if (arguments.length < 2) - throw Error('Baobab.set: expects a key and a value.'); - - var spec = {}; - - if (type.Array(key)) { - var path = helpers.solvePath(this.data, key); - - if (!path) - throw Error('Baobab.set: could not solve dynamic path.'); - - spec = helpers.pathObject(path, {$set: val}); - } - else { - spec[key] = {$set: val}; - } - - return this.update(spec); -}; - -Baobab.prototype.unset = function(key) { - if (!key && key !== 0) - throw Error('Baobab.unset: expects a valid key to unset.'); - - var spec = {}; - spec[key] = {$unset: true}; - - return this.update(spec); -}; - -Baobab.prototype.update = function(spec) { +Baobab.prototype.stack = function(spec) { var self = this; if (!type.Object(spec)) @@ -289,33 +145,63 @@ Baobab.prototype.update = function(spec) { return this; }; -Baobab.prototype.hasHistory = function() { - return !!this._history.length; -}; +Baobab.prototype.commit = function() { + var self = this; -Baobab.prototype.getHistory = function() { - return this._history; -}; + // Applying modifications + var result = update(this.data, this._transaction, this.options); + + var oldData = this.data; + + // Resetting + this._transaction = {}; -Baobab.prototype.undo = function() { - if (!this.hasHistory()) - throw Error('Baobab.undo: no history recorded, cannot undo.'); + if (this._future) + this._future = clearTimeout(this._future); + + // Validate? + var validate = this.options.validate, + behavior = this.options.validationBehavior; - var lastRecord = this._history.shift(); - this.commit(lastRecord); + if (typeof validate === 'function') { + var error = validate.call(this, oldData, result.data, result.log); + + if (error instanceof Error) { + this.emit('invalid', {error: error}); + + if (behavior === 'rollback') + return this; + } + } + + // Switching tree's data + this.data = result.data; + + // Baobab-level update event + this.emit('update', { + log: result.log, + previousState: oldData + }); + + return this; }; Baobab.prototype.release = function() { + var k; delete this.data; delete this._transaction; - delete this._history; // Releasing cursors - for (var k in this._cursors) + for (k in this._cursors) this._cursors[k].release(); delete this._cursors; + // Releasing facets + for (k in this.facets) + this.facets[k].release(); + delete this.facets; + // Killing event emitter this.kill(); }; @@ -324,7 +210,11 @@ Baobab.prototype.release = function() { * Output */ Baobab.prototype.toJSON = function() { - return this.reference(); + return this.get(); +}; + +Baobab.prototype.toString = function() { + return this._identity; }; /** diff --git a/src/combination.js b/src/combination.js deleted file mode 100644 index ce4d349..0000000 --- a/src/combination.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Baobab Cursor Combination - * ========================== - * - * A useful abstraction dealing with cursor's update logical combinations. - */ -var EventEmitter = require('emmett'), - helpers = require('./helpers.js'), - type = require('./type.js'); - -/** - * Utilities - */ -function bindCursor(c, cursor) { - cursor.on('update', c.cursorListener); - c.tree.off('update', c.treeListener); - c.tree.on('update', c.treeListener); -} - -/** - * Main Class - */ -function Combination(operator /*, &cursors */) { - var self = this; - - // Safeguard - if (arguments.length < 2) - throw Error('baobab.Combination: not enough arguments.'); - - var first = arguments[1], - rest = helpers.arrayOf(arguments).slice(2); - - if (first instanceof Array) { - rest = first.slice(1); - first = first[0]; - } - - if (!type.Cursor(first)) - throw Error('baobab.Combination: argument should be a cursor.'); - - if (operator !== 'or' && operator !== 'and') - throw Error('baobab.Combination: invalid operator.'); - - // Extending event emitter - EventEmitter.call(this); - - // Properties - this.cursors = [first]; - this.operators = []; - this.tree = first.tree; - - // State - this.updates = new Array(this.cursors.length); - - // Listeners - this.cursorListener = function() { - self.updates[self.cursors.indexOf(this)] = true; - }; - - this.treeListener = function() { - var shouldFire = self.updates[0], - i, - l; - - for (i = 1, l = self.cursors.length; i < l; i++) { - shouldFire = self.operators[i - 1] === 'or' ? - shouldFire || self.updates[i] : - shouldFire && self.updates[i]; - } - - if (shouldFire) - self.emit('update'); - - // Waiting for next update - self.updates = new Array(self.cursors.length); - }; - - // Lazy binding - this.bound = false; - - var regularOn = this.on, - regularOnce = this.once; - - var lazyBind = function() { - if (self.bound) - return; - self.bound = true; - self.cursors.forEach(function(cursor) { - bindCursor(self, cursor); - }); - }; - - this.on = function() { - lazyBind(); - return regularOn.apply(this, arguments); - }; - - this.once = function() { - lazyBind(); - return regularOnce.apply(this, arguments); - }; - - // Attaching any other passed cursors - rest.forEach(function(cursor) { - this[operator](cursor); - }, this); -} - -helpers.inherits(Combination, EventEmitter); - -/** - * Prototype - */ -function makeOperator(operator) { - Combination.prototype[operator] = function(cursor) { - - // Safeguard - if (!type.Cursor(cursor)) { - this.release(); - throw Error('baobab.Combination.' + operator + ': argument should be a cursor.'); - } - - if (~this.cursors.indexOf(cursor)) { - this.release(); - throw Error('baobab.Combination.' + operator + ': cursor already in combination.'); - } - - this.cursors.push(cursor); - this.operators.push(operator); - this.updates.length++; - - if (this.bound) - bindCursor(this, cursor); - - return this; - }; -} - -makeOperator('or'); -makeOperator('and'); - -Combination.prototype.release = function() { - - // Dropping cursors listeners - this.cursors.forEach(function(cursor) { - cursor.off('update', this.cursorListener); - }, this); - - // Dropping tree listener - this.tree.off('update', this.treeListener); - - // Cleaning - this.cursors = null; - this.operators = null; - this.tree = null; - this.updates = null; - - // Dropping own listeners - this.kill(); -}; - -/** - * Exporting - */ -module.exports = Combination; diff --git a/src/cursor.js b/src/cursor.js index a2e6134..42fea1a 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -5,9 +5,8 @@ * Nested selection into a baobab tree. */ var EventEmitter = require('emmett'), - Combination = require('./combination.js'), - mixins = require('./mixins.js'), helpers = require('./helpers.js'), + defaults = require('../defaults.js'), type = require('./type.js'); /** @@ -26,52 +25,59 @@ function Cursor(tree, path, solvedPath, hash) { this.tree = tree; this.path = path; this.hash = hash; - this.relevant = this.reference() !== undefined; + this.archive = null; + this.recording = false; + this.undoing = false; + + // Privates + this._identity = '[object Cursor]'; // Complex path? this.complexPath = !!solvedPath; this.solvedPath = this.complexPath ? solvedPath : this.path; + // Relevant? + this.relevant = this.get() !== undefined; + // Root listeners + function update(previousState) { + if (self.recording && !self.undoing) { + + // Handle archive + var data = helpers.getIn(previousState, self.solvedPath, self.tree), + record = helpers.deepClone(data); + + self.archive.add(record); + } + + self.undoing = false; + return self.emit('update'); + } + this.updateHandler = function(e) { var log = e.data.log, + previousState = e.data.previousState, shouldFire = false, c, p, l, m, i, j; // Solving path if needed if (self.complexPath) - self.solvedPath = helpers.solvePath(self.tree.data, self.path); + self.solvedPath = helpers.solvePath(self.tree.data, self.path, self.tree); // If selector listens at tree, we fire if (!self.path.length) - return self.emit('update'); + return update(previousState); // Checking update log to see whether the cursor should update. - outer: - for (i = 0, l = log.length; i < l; i++) { - c = log[i]; - - for (j = 0, m = c.length; j < m; j++) { - p = c[j]; - - // If path is not relevant to us, we break - if (p !== '' + self.solvedPath[j]) - break; - - // If we reached last item and we are relevant, we fire - if (j + 1 === m || j + 1 === self.solvedPath.length) { - shouldFire = true; - break outer; - } - } - } + if (self.solvedPath) + shouldFire = helpers.solveUpdate(log, [self.solvedPath]); // Handling relevancy - var data = self.reference() !== undefined; + var data = self.get() !== undefined; if (self.relevant) { if (data && shouldFire) { - self.emit('update'); + update(previousState); } else if (!data) { self.emit('irrelevant'); @@ -81,36 +87,27 @@ function Cursor(tree, path, solvedPath, hash) { else { if (data && shouldFire) { self.emit('relevant'); - self.emit('update'); + update(previousState); self.relevant = true; } } }; - // Making mixin - this.mixin = mixins.cursor(this); - // Lazy binding - var bound = false, - regularOn = this.on, - regularOnce = this.once; + var bound = false; - var lazyBind = function() { + this._lazyBind = function() { if (bound) return; bound = true; self.tree.on('update', self.updateHandler); }; - this.on = function() { - lazyBind(); - return regularOn.apply(this, arguments); - }; + this.on = helpers.before(this._lazyBind, this.on.bind(this)); + this.once = helpers.before(this._lazyBind, this.once.bind(this)); - this.once = function() { - lazyBind(); - return regularOnce.apply(this, arguments); - }; + if (this.complexPath) + this._lazyBind(); } helpers.inherits(Cursor, EventEmitter); @@ -123,7 +120,7 @@ Cursor.prototype.isRoot = function() { }; Cursor.prototype.isLeaf = function() { - return type.Primitive(this.reference()); + return type.Primitive(this.get()); }; Cursor.prototype.isBranch = function() { @@ -179,7 +176,7 @@ Cursor.prototype.right = function() { if (isNaN(last)) throw Error('baobab.Cursor.right: cannot go right on a non-list type.'); - if (last + 1 === this.up().reference().length) + if (last + 1 === this.up().get().length) return null; return this.tree.select(this.solvedPath.slice(0, -1).concat(last + 1)); @@ -191,7 +188,7 @@ Cursor.prototype.rightmost = function() { if (isNaN(last)) throw Error('baobab.Cursor.right: cannot go right on a non-list type.'); - var list = this.up().reference(); + var list = this.up().get(); return this.tree.select(this.solvedPath.slice(0, -1).concat(list.length - 1)); }; @@ -199,7 +196,7 @@ Cursor.prototype.rightmost = function() { Cursor.prototype.down = function() { var last = +this.solvedPath[this.solvedPath.length - 1]; - if (!(this.reference() instanceof Array)) + if (!(this.get() instanceof Array)) return null; return this.tree.select(this.solvedPath.concat(0)); @@ -212,142 +209,150 @@ Cursor.prototype.get = function(path) { if (arguments.length > 1) path = helpers.arrayOf(arguments); - if (type.Step(path)) - return this.tree.get(this.solvedPath.concat(path)); - else - return this.tree.get(this.solvedPath); -}; - -Cursor.prototype.reference = function(path) { - if (arguments.length > 1) - path = helpers.arrayOf(arguments); - - if (type.Step(path)) - return this.tree.reference(this.solvedPath.concat(path)); - else - return this.tree.reference(this.solvedPath); -}; + var fullPath = this.solvedPath.concat( + [].concat(path || path === 0 ? path : []) + ); -Cursor.prototype.clone = function(path) { - if (arguments.length > 1) - path = helpers.arrayOf(arguments); - - if (type.Step(path)) - return this.tree.clone(this.solvedPath.concat(path)); - else - return this.tree.clone(this.solvedPath); + return helpers.getIn(this.tree.data, fullPath, this.tree); }; /** * Update */ -Cursor.prototype.set = function(key, val) { - if (arguments.length < 2) - throw Error('baobab.Cursor.set: expecting at least key/value.'); +function pathPolymorphism(method, allowedType, key, val) { + if (arguments.length > 5) + throw Error('baobab.Cursor.' + method + ': too many arguments.'); - var data = this.reference(); + if (arguments.length < 4) { + val = key; + key = []; + } - if (typeof data !== 'object') - throw Error('baobab.Cursor.set: trying to set key to a non-object.'); + key = key || []; - var spec = {}; + // Splice exception + if (method === 'splice' && + !type.Splicer(val)) { + if (type.Array(val)) + val = [val]; + else + throw Error('baobab.Cursor.splice: incorrect value.'); + } - if (type.Array(key)) { - var path = helpers.solvePath(data, key); + // Checking value validity + if (allowedType && !allowedType(val)) + throw Error('baobab.Cursor.' + method + ': incorrect value.'); - if (!path) - throw Error('baobab.Cursor.set: could not solve dynamic path.'); + var path = [].concat(key), + solvedPath = helpers.solvePath(this.get(), path, this.tree); - spec = helpers.pathObject(path, {$set: val}); - } - else { - spec[key] = {$set: val}; - } + if (!solvedPath) + throw Error('baobab.Cursor.' + method + ': could not solve dynamic path.'); - return this.update(spec); -}; + var leaf = {}; + leaf['$' + method] = val; -Cursor.prototype.edit = function(val) { - return this.update({$set: val}); -}; + var spec = helpers.pathObject(solvedPath, leaf); + + return spec; +} + +function makeUpdateMethod(command, type) { + Cursor.prototype[command] = function() { + var spec = pathPolymorphism.bind(this, command, type).apply(this, arguments); + + return this.update(spec); + }; +} + +makeUpdateMethod('set'); +makeUpdateMethod('apply', type.Function); +makeUpdateMethod('chain', type.Function); +makeUpdateMethod('push'); +makeUpdateMethod('unshift'); +makeUpdateMethod('merge', type.Object); +makeUpdateMethod('splice'); Cursor.prototype.unset = function(key) { - if (!key && key !== 0) - throw Error('baobab.Cursor.unset: expects a valid key to unset.'); + if (key === undefined && this.isRoot()) + throw Error('baobab.Cursor.unset: cannot remove root node.'); - if (typeof this.reference() !== 'object') - throw Error('baobab.Cursor.set: trying to set key to a non-object.'); + var spec = pathPolymorphism.bind(this, 'unset', null).apply(this, [key, true]); - var spec = {}; - spec[key] = {$unset: true}; return this.update(spec); }; -Cursor.prototype.remove = function() { - if (this.isRoot()) - throw Error('baobab.Cursor.remove: cannot remove root node.'); +Cursor.prototype.update = function(key, spec) { + if (arguments.length < 2) { + this.tree.stack(helpers.pathObject(this.solvedPath, key)); + return this; + } - return this.update({$unset: true}); -}; + // Solving path + var path = [].concat(key), + solvedPath = helpers.solvePath(this.get(), path, this.tree); -Cursor.prototype.apply = function(fn) { - if (typeof fn !== 'function') - throw Error('baobab.Cursor.apply: argument is not a function.'); + if (!solvedPath) + throw Error('baobab.Cursor.update: could not solve dynamic path.'); - return this.update({$apply: fn}); + this.tree.stack(helpers.pathObject(this.solvedPath.concat(solvedPath), spec)); + return this; }; -Cursor.prototype.chain = function(fn) { - if (typeof fn !== 'function') - throw Error('baobab.Cursor.chain: argument is not a function.'); +/** + * History + */ +Cursor.prototype.startRecording = function(maxRecords) { + maxRecords = maxRecords || 5; - return this.update({$chain: fn}); -}; + if (maxRecords < 1) + throw Error('baobab.Cursor.startRecording: invalid maximum number of records.'); -Cursor.prototype.push = function(value) { - if (!(this.reference() instanceof Array)) - throw Error('baobab.Cursor.push: trying to push to non-array value.'); + if (this.archive) + return this; - if (arguments.length > 1) - return this.update({$push: helpers.arrayOf(arguments)}); - else - return this.update({$push: value}); -}; + // Lazy bind + this._lazyBind(); -Cursor.prototype.unshift = function(value) { - if (!(this.reference() instanceof Array)) - throw Error('baobab.Cursor.push: trying to push to non-array value.'); + this.archive = helpers.archive(maxRecords); + this.recording = true; + return this; +}; - if (arguments.length > 1) - return this.update({$unshift: helpers.arrayOf(arguments)}); - else - return this.update({$unshift: value}); +Cursor.prototype.stopRecording = function() { + this.recording = false; + return this; }; -Cursor.prototype.merge = function(o) { - if (!type.Object(o)) - throw Error('baobab.Cursor.merge: trying to merge a non-object.'); +Cursor.prototype.undo = function(steps) { + steps = steps || 1; - if (!type.Object(this.reference())) - throw Error('baobab.Cursor.merge: trying to merge into a non-object.'); + if (!this.recording) + throw Error('baobab.Cursor.undo: cursor is not recording.'); - this.update({$merge: o}); + if (!type.PositiveInteger(steps)) + throw Error('baobab.Cursor.undo: expecting a positive integer.'); + + var record = this.archive.back(steps); + + if (!record) + throw Error('boabab.Cursor.undo: cannot find a relevant record (' + steps + ' back).'); + + this.undoing = true; + return this.set(record); }; -Cursor.prototype.update = function(spec) { - this.tree.update(helpers.pathObject(this.solvedPath, spec)); - return this; +Cursor.prototype.hasHistory = function() { + return !!(this.archive && this.archive.get().length); }; -/** - * Combination - */ -Cursor.prototype.or = function(otherCursor) { - return new Combination('or', this, otherCursor); +Cursor.prototype.getHistory = function() { + return this.archive ? this.archive.get() : []; }; -Cursor.prototype.and = function(otherCursor) { - return new Combination('and', this, otherCursor); +Cursor.prototype.clearHistory = function() { + this.archive = null; + return this; }; /** @@ -366,6 +371,7 @@ Cursor.prototype.release = function() { delete this.tree; delete this.path; delete this.solvedPath; + delete this.archive; // Killing emitter this.kill(); @@ -375,11 +381,11 @@ Cursor.prototype.release = function() { * Output */ Cursor.prototype.toJSON = function() { - return this.reference(); + return this.get(); }; -type.Cursor = function (value) { - return value instanceof Cursor; +Cursor.prototype.toString = function() { + return this._identity; }; /** diff --git a/src/facet.js b/src/facet.js new file mode 100644 index 0000000..d7a30c7 --- /dev/null +++ b/src/facet.js @@ -0,0 +1,157 @@ +/** + * Baobab Facet Abstraction + * ========================= + * + * Facets enable the user to define views on a given Baobab tree. + */ +var EventEmitter = require('emmett'), + Cursor = require('./cursor.js'), + helpers = require('./helpers.js'), + type = require('./type.js'); + +function Facet(tree, definition, scope) { + var self = this; + + var firstTime = true, + solved = false, + getter = definition.get, + data = null; + + // Extending event emitter + EventEmitter.call(this); + + // Properties + this.tree = tree; + this.cursors = {}; + this.facets = {}; + + var cursorsMapping = definition.cursors, + facetsMapping = definition.facets, + complexCursors = typeof definition.cursors === 'function', + complexFacets = typeof definition.facets === 'function'; + + // Refreshing the internal mapping + function refresh(complexity, targetMapping, targetProperty, mappingType) { + if (!complexity && !firstTime) + return; + + solved = false; + + var solvedMapping = targetMapping; + + if (complexity) + solvedMapping = targetMapping.call(scope); + + if (!mappingType(solvedMapping)) + throw Error('baobab.Facet: incorrect ' + targetProperty + ' mapping.'); + + self[targetProperty] = {}; + + Object.keys(solvedMapping).forEach(function(k) { + + if (targetProperty === 'cursors') { + if (solvedMapping[k] instanceof Cursor) { + self.cursors[k] = solvedMapping[k]; + return; + } + + if (type.Path(solvedMapping[k])) { + self.cursors[k] = tree.select(solvedMapping[k]); + return; + } + } + + else { + if (solvedMapping[k] instanceof Facet) { + self.facets[k] = solvedMapping[k]; + return; + } + + if (typeof solvedMapping[k] === 'string') { + self.facets[k] = tree.facets[solvedMapping[k]]; + + if (!self.facets) + throw Error('baobab.Facet: unkown "' + solvedMapping[k] + '" facet in facets mapping.'); + return; + } + } + + throw Error('baobab.Facet: invalid value returned by function in ' + targetProperty + ' mapping.'); + }); + } + + this.refresh = function() { + + if (cursorsMapping) + refresh( + complexCursors, + cursorsMapping, + 'cursors', + type.FacetCursors + ); + + if (facetsMapping) + refresh( + complexFacets, + facetsMapping, + 'facets', + type.FacetFacets + ); + }; + + // Data solving + this.get = function() { + if (solved) + return data; + + // Solving + var data = {}, + k; + + for (k in self.facets) + data[k] = self.facets[k].get(); + + for (k in self.cursors) + data[k] = self.cursors[k].get(); + + // Applying getter + data = typeof getter === 'function' ? + getter.call(null, data) : + data; + + solved = true; + + return data; + }; + + // Tracking the tree's updates + this.updateHandler = function(e) { + var paths = Object.keys(self.cursors).map(function(k) { + return self.cursors[k].solvedPath; + }) + + if (helpers.solveUpdate(e.data.log, paths)) { + solved = false; + self.emit('update'); + } + }; + + // Init routine + this.refresh(); + this.tree.on('update', this.updateHandler); + + firstTime = false; +} + +helpers.inherits(Facet, EventEmitter); + +Facet.prototype.release = function() { + this.tree.off('update', this.updateHandler); + + this.tree = null; + this.cursors = null; + this.facets = null; + this.kill(); +}; + +module.exports = Facet; diff --git a/src/helpers.js b/src/helpers.js index a7a1c4e..34aeebc 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -11,6 +11,26 @@ function arrayOf(o) { return Array.prototype.slice.call(o); } +// Decorate a function by applying something before it +function before(decorator, fn) { + return function() { + decorator(); + fn.apply(null, arguments); + }; +} + +// Non-mutative splice function +function splice(array, index, nb /*, &elements */) { + var elements = arrayOf(arguments).slice(3); + + index = +index; + nb = +nb; + + return array + .slice(0, index) + .concat(array.slice(index + nb).concat(elements)); +} + // Shallow merge function shallowMerge(o1, o2) { var o = {}, @@ -151,10 +171,11 @@ function indexByComparison(object, spec) { } // Retrieve nested objects -function getIn(object, path) { +function getIn(object, path, tree) { path = path || []; var c = object, + p, i, l; @@ -169,10 +190,21 @@ function getIn(object, path) { c = first(c, path[i]); } else if (typeof path[i] === 'object') { - if (!type.Array(c)) + if (tree && '$cursor' in path[i]) { + if (!type.Path(path[i].$cursor)) + throw Error('baobab.getIn: $cursor path must be an array.'); + + p = tree.get(path[i].$cursor); + c = c[p]; + } + + else if (!type.Array(c)) { return; + } - c = firstByComparison(c, path[i]); + else { + c = firstByComparison(c, path[i]); + } } else { c = c[path[i]]; @@ -183,7 +215,7 @@ function getIn(object, path) { } // Solve a complex path -function solvePath(object, path) { +function solvePath(object, path, tree) { var solvedPath = [], c = object, idx, @@ -203,12 +235,24 @@ function solvePath(object, path) { c = c[idx]; } else if (typeof path[i] === 'object') { - if (!type.Array(c)) + if (tree && '$cursor' in path[i]) { + if (!type.Path(path[i].$cursor)) + throw Error('baobab.getIn: $cursor path must be an array.'); + + p = tree.get(path[i].$cursor); + solvedPath.push(p); + c = c[p]; + } + + else if (!type.Array(c)) { return; + } - idx = indexByComparison(c, path[i]); - solvedPath.push(idx); - c = c[idx]; + else { + idx = indexByComparison(c, path[i]); + solvedPath.push(idx); + c = c[idx]; + } } else { solvedPath.push(path[i]); @@ -219,6 +263,40 @@ function solvePath(object, path) { return solvedPath; } +// Determine whether an update should fire for the given paths +// NOTES: 1) if performance becomes an issue, the threefold loop can be +// simplified to become a complex twofold one. +// 2) a regex version could also work but I am not confident it would be +// faster. +function solveUpdate(log, paths) { + var i, j, k, l, m, n, p, c, s; + + // Looping through possible paths + for (i = 0, l = paths.length; i < l; i++) { + p = paths[i]; + + // Looping through logged paths + for (j = 0, m = log.length; j < m; j++) { + c = log[j]; + + // Looping through steps + for (k = 0, n = c.length; k < n; k++) { + s = c[k]; + + // If path is not relevant, we break + if (s != p[k]) + break; + + // If we reached last item and we are relevant + if (k + 1 === n || k + 1 === p.length) + return true; + } + } + } + + return false; +} + // Return a fake object relative to the given path function pathObject(path, spec) { var l = path.length, @@ -237,6 +315,7 @@ function pathObject(path, spec) { return o; } +// Shim used for cross-compatible event emitting extension function inherits(ctor, superCtor) { ctor.super_ = superCtor; var TempCtor = function () {}; @@ -245,8 +324,34 @@ function inherits(ctor, superCtor) { ctor.prototype.constructor = ctor; } +// Archive +function archive(size) { + var records = []; + + return { + add: function(record) { + records.unshift(record); + + if (records.length > size) + records.length = size; + }, + back: function(steps) { + var record = records[steps - 1]; + + if (record) + records = records.slice(steps); + return record; + }, + get: function() { + return records; + } + }; +} + module.exports = { + archive: archive, arrayOf: arrayOf, + before: before, deepClone: deepClone, shallowClone: shallowClone, shallowMerge: shallowMerge, @@ -254,5 +359,7 @@ module.exports = { getIn: getIn, inherits: inherits, pathObject: pathObject, - solvePath: solvePath + solvePath: solvePath, + solveUpdate: solveUpdate, + splice: splice }; diff --git a/src/merge.js b/src/merge.js index bf47520..06c2f22 100644 --- a/src/merge.js +++ b/src/merge.js @@ -8,105 +8,62 @@ var helpers = require('./helpers.js'), type = require('./type.js'); // Helpers -function hasKey(o, key) { - return key in (o || {}); -} +var COMMANDS = ['$unset', '$set', '$merge', '$apply']; + +// TODO: delete every keys +function only(o, n, keep) { + COMMANDS.forEach(function(c) { + if (keep !== c) + delete o[c]; + }); -function conflict(a, b, key) { - return hasKey(a, key) && hasKey(b, key); + o[keep] = n[keep]; } // Main function -function merge() { - var res = {}, - current, - next, - l = arguments.length, - i, - k; +// TODO: use a better way than shallow cloning b? +function merge(a, b) { + var o = helpers.shallowClone(b || {}), + k, + i; - for (i = l - 1; i >= 0; i--) { + COMMANDS.forEach(function(c) { + if (a[c]) + only(o, a, c); + }); - // Upper $set/$apply... and conflicts - // When solving conflicts, here is the priority to apply: - // -- 0) $unset - // -- 1) $set - // -- 2) $merge - // -- 3) $apply - // -- 4) $chain - if (arguments[i].$unset) { - delete res.$set; - delete res.$apply; - delete res.$merge; - res.$unset = arguments[i].$unset; - } - else if (arguments[i].$set) { - delete res.$apply; - delete res.$merge; - delete res.$unset; - res.$set = arguments[i].$set; - continue; - } - else if (arguments[i].$merge) { - delete res.$set; - delete res.$apply; - delete res.$unset; - res.$merge = arguments[i].$merge; - continue; - } - else if (arguments[i].$apply){ - delete res.$set; - delete res.$merge; - delete res.$unset; - res.$apply = arguments[i].$apply; - continue; - } - else if (arguments[i].$chain) { - delete res.$set; - delete res.$merge; - delete res.$unset; + if (a.$chain) { + COMMANDS.slice(0, -1).forEach(function(c) { + delete o[c]; + }); - if (res.$apply) - res.$apply = helpers.compose(res.$apply, arguments[i].$chain); - else - res.$apply = arguments[i].$chain; - continue; - } + if (o.$apply) + o.$apply = helpers.compose(o.$apply, a.$chain); + else + o.$apply = a.$chain; + } - for (k in arguments[i]) { - current = res[k]; - next = arguments[i][k]; + if (a.$splice || o.$splice) { + o.$splice = [].concat(o.$splice || []).concat(a.$splice || []); + } - if (current && type.Object(next)) { + if (a.$push || o.$push) { + o.$push = [].concat(o.$push || []).concat(a.$push || []); + } - // $push conflict - if (conflict(current, next, '$push')) { - if (type.Array(current.$push)) - current.$push = current.$push.concat(next.$push); - else - current.$push = [current.$push].concat(next.$push); - } + if (a.$unshift || o.$unshift) { + o.$unshift = [].concat(a.$unshift || []).concat(o.$unshift || []); + } - // $unshift conflict - else if (conflict(current, next, '$unshift')) { - if (type.Array(next.$unshift)) - current.$unshift = next.$unshift.concat(current.$unshift); - else - current.$unshift = [next.$unshift].concat(current.$unshift); - } + for (k in a) { - // No conflict - else { - res[k] = merge(next, current); - } - } - else { - res[k] = next; - } - } + if (type.Object(a[k])) + o[k] = merge(a[k], o[k]); + else if (k[0] !== '$') + o[k] = a[k]; } - return res; + return o; } module.exports = merge; diff --git a/src/mixins.js b/src/mixins.js deleted file mode 100644 index cce7a15..0000000 --- a/src/mixins.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Baobab React Mixins - * ==================== - * - * Compilation of react mixins designed to deal with cursors integration. - */ -var Combination = require('./combination.js'), - type = require('./type.js'); - -module.exports = { - baobab: function(baobab) { - return { - - // Run Baobab mixin first to allow mixins to access cursors - mixins: [{ - getInitialState: function() { - - // Binding baobab to instance - this.tree = baobab; - - // Is there any cursors to create? - if (!this.cursor && !this.cursors) - return {}; - - // Is there conflicting definitions? - if (this.cursor && this.cursors) - throw Error('baobab.mixin: you cannot have both ' + - '`component.cursor` and `component.cursors`. Please ' + - 'make up your mind.'); - - // Type - this.__type = null; - - // Making update handler - this.__updateHandler = (function() { - this.setState(this.__getCursorData()); - }).bind(this); - - if (this.cursor) { - if (!type.MixinCursor(this.cursor)) - throw Error('baobab.mixin.cursor: invalid data (cursor, ' + - 'string, array or function).'); - - if (type.Function(this.cursor)) - this.cursor = this.cursor(); - - if (!type.Cursor(this.cursor)) - this.cursor = baobab.select(this.cursor); - - this.__getCursorData = (function() { - return {cursor: this.cursor.get()}; - }).bind(this); - this.__type = 'single'; - } - else if (this.cursors) { - if (!type.MixinCursors(this.cursors)) - throw Error('baobab.mixin.cursor: invalid data (object, array or function).'); - - if (type.Function(this.cursors)) - this.cursors = this.cursors(); - - if (type.Array(this.cursors)) { - this.cursors = this.cursors.map(function(path) { - return type.Cursor(path) ? path : baobab.select(path); - }); - - this.__getCursorData = (function() { - return {cursors: this.cursors.map(function(cursor) { - return cursor.get(); - })}; - }).bind(this); - this.__type = 'array'; - } - else { - for (var k in this.cursors) { - if (!type.Cursor(this.cursors[k])) - 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'; - } - } - - return this.__getCursorData(); - }, - componentDidMount: function() { - if (this.__type === 'single') { - this.__combination = new Combination('or', [this.cursor]); - this.__combination.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.__combination) - this.__combination.release(); - } - }].concat(baobab.options.mixins) - }; - }, - cursor: function(cursor) { - return { - - // Run cursor mixin first to allow mixins to access cursors - mixins: [{ - getInitialState: function() { - - // Binding cursor to instance - this.cursor = cursor; - - // Making update handler - this.__updateHandler = (function() { - this.setState({cursor: this.cursor.get()}); - }).bind(this); - - 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.tree.options.mixins) - }; - } -}; diff --git a/src/type.js b/src/type.js index c8b7cee..d742215 100644 --- a/src/type.js +++ b/src/type.js @@ -7,108 +7,110 @@ * * @christianalfoni */ +var type = {}; -// Not reusing methods as it will just be an extra -// call on the stack -var type = function (value) { - if (Array.isArray(value)) { - return 'array'; - } else if (typeof value === 'object' && value !== null) { - return 'object'; - } else if (typeof value === 'string') { - return 'string'; - } else if (typeof value === 'number') { - return 'number'; - } else if (typeof value === 'boolean') { - return 'boolean'; - } else if (typeof value === 'function') { - return 'function'; - } else if (value === null) { - return 'null'; - } else if (value === undefined) { - return 'undefined'; - } else if (value instanceof Date) { - return 'date'; - } else { - return 'invalid'; - } -}; +/** + * Helpers + */ +function anyOf(value, allowed) { + return allowed.some(function(t) { + return type[t](value); + }); +} -type.Array = function (value) { +/** + * Simple types + */ +type.Array = function(value) { return Array.isArray(value); }; -type.Object = function (value) { - return !Array.isArray(value) && typeof value === 'object' && value !== null; +type.Object = function(value) { + return value && + typeof value === 'object' && + !Array.isArray(value) && + !(value instanceof Function); }; -type.String = function (value) { +type.String = function(value) { return typeof value === 'string'; }; -type.Number = function (value) { +type.Number = function(value) { return typeof value === 'number'; }; -type.Boolean = function (value) { - return typeof value === 'boolean'; +type.PositiveInteger = function(value) { + return typeof value === 'number' && value > 0 && value % 1 === 0; }; -type.Function = function (value) { +type.Function = function(value) { return typeof value === 'function'; }; -type.Primitive = function (value) { - return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'; +type.Primitive = function(value) { + return typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean'; }; -type.Date = function (value) { +type.Date = function(value) { return value instanceof Date; }; -type.Step = function (value) { - var valueType = type(value); - var notValid = ['null', 'undefined', 'invalid', 'date']; - return notValid.indexOf(valueType) === -1; +/** + * Complex types + */ +type.NonScalar = function(value) { + return type.Object(value) || type.Array(value); +}; + +type.Splicer = function(value) { + return type.Array(value) && + value.every(type.Array); }; -// Should undefined be allowed? -type.Path = function (value) { - var types = ['object', 'string', 'number', 'function', 'undefined']; +type.Path = function(value) { + var allowed = ['String', 'Number', 'Function', 'Object']; + if (type.Array(value)) { - for (var x = 0; x < value.length; x++) { - if (types.indexOf(type(value[x])) === -1) { - return false; - } - } - } else { - return types.indexOf(type(value)) >= 0; + return value.every(function(step) { + return anyOf(step, allowed); + }); + } + else { + return anyOf(value, allowed); } - return true; - }; -// string|number|array|cursor|function -type.MixinCursor = function (value) { - var allowedValues = ['string', 'number', 'array', 'function']; - return allowedValues.indexOf(type(value)) >= 0 || type.Cursor(value); +type.ComplexPath = function(value) { + return value.some(function(step) { + return anyOf(step, ['Object', 'Function']); + }); }; -// array|object|function -type.MixinCursors = function (value) { - var allowedValues = ['array', 'object', 'function']; - return allowedValues.indexOf(type(value)) >= 0; -}; +type.FacetCursors = function(value) { + if (!type.Object(value)) + return false; -// Already know this is an array -type.ComplexPath = function (value) { - var complexTypes = ['object', 'function']; - for (var x = 0; x < value.length; x++) { - if (complexTypes.indexOf(type(value[x])) >= 0) { - return true; - } - } - return false; + return Object.keys(value).every(function(k) { + var v = value[k]; + + return type.Path(v) || + v instanceof require('./cursor.js'); + }); }; +type.FacetFacets = function(value) { + if (!type.Object(value)) + return false; + + return Object.keys(value).every(function(k) { + var v = value[k]; + + return typeof v === 'string' || + v instanceof require('./facet.js'); + }); +} + module.exports = type; diff --git a/src/update.js b/src/update.js index 1625b8e..a5ad012 100644 --- a/src/update.js +++ b/src/update.js @@ -8,166 +8,150 @@ var helpers = require('./helpers.js'), type = require('./type.js'); -var COMMANDS = {}; -[ - '$set', - '$push', - '$unshift', - '$apply', - '$merge' -].forEach(function(c) { - COMMANDS[c] = true; -}); - // Helpers function makeError(path, message) { var e = new Error('baobab.update: ' + message + ' at path /' + - path.toString()); + path.slice(1).join('/')); e.path = path; return e; } -// Core function -function update(target, spec, opts) { - opts = opts || {shiftReferences: false}; +module.exports = function(data, spec, opts) { + opts = opts || {}; + + if (!type.Object(data) && !type.Array(data)) + throw Error('baobab.update: invalid target.'); + var log = {}; - // Closure mutating the internal object - (function mutator(o, spec, path, parent) { - path = path || []; + // Shifting root + data = {root: helpers.shallowClone(data)}; + + // Closure performing the updates themselves + var mutator = function(o, spec, path, parent) { + path = path || ['root']; - var hash = path.join('λ'), + var hash = path.join('|λ|'), + lastKey = path[path.length - 1], fn, - h, k, v; - for (k in spec) { - if (COMMANDS[k]) { - v = spec[k]; - - // Logging update - log[hash] = true; - - // TODO: this could be before in the recursion - // Applying - switch (k) { - case '$push': - if (!type.Array(o)) - throw makeError(path, 'using command $push to a non array'); - - if (!type.Array(v)) - o.push(v); - else - o.push.apply(o, v); - break; - case '$unshift': - if (!type.Array(o)) - throw makeError(path, 'using command $unshift to a non array'); - - if (!type.Array(v)) - o.unshift(v); - else - o.unshift.apply(o, v); - break; - } - } - else { - h = hash ? hash + 'λ' + k : k; - - if ('$unset' in (spec[k] || {})) { - - // Logging update - log[h] = true; - - if (type.Array(o)) { - if (!opts.shiftReferences) - o.splice(k, 1); - else - parent[path[path.length - 1]] = o.slice(0, +k).concat(o.slice(+k + 1)); - } - else { - delete o[k]; - } + var leafLevel = Object.keys(spec).some(function(k) { + return !!~['$set', '$push', '$unshift', '$splice', '$unset', '$merge', '$apply'].indexOf(k); + }); + + if (leafLevel) { + log[hash] = true; + + for (k in spec) { + + // $unset + if (k === '$unset') { + var olderKey = path[path.length - 2]; + + if (!type.Object(parent[olderKey])) + throw makeError(path.slice(0, -1), 'using command $unset on a non-object'); + + parent[olderKey] = helpers.shallowClone(o); + delete parent[olderKey][lastKey]; + + break; } - else if ('$set' in (spec[k] || {})) { - v = spec[k].$set; - // Logging update - log[h] = true; - o[k] = v; + // $set + if (k === '$set') { + v = spec.$set; + + o[lastKey] = v; + break; } - else if ('$apply' in (spec[k] || {}) || '$chain' in (spec[k] || {})) { - // TODO: this should not happen likewise. - fn = spec[k].$apply || spec[k].$chain; + // $apply + if (k === '$apply') { + fn = spec.$apply; if (typeof fn !== 'function') - throw makeError(path.concat(k), 'using command $apply with a non function'); + throw makeError(path, 'using command $apply with a non function'); + + o[lastKey] = fn.call(null, o[lastKey]); + break; + } + + // $merge + if (k === '$merge') { + v = spec.$merge; - // Logging update - log[h] = true; - o[k] = fn.call(null, o[k]); + if (!type.Object(o[lastKey]) || !type.Object(v)) + throw makeError(path, 'using command $merge with a non object'); + + o[lastKey] = helpers.shallowMerge(o[lastKey], v); + break; } - else if ('$merge' in (spec[k] || {})) { - v = spec[k].$merge; - if (!type.Object(o[k])) - throw makeError(path.concat(k), 'using command $merge on a non-object'); + // $splice + if (k === '$splice') { + v = spec.$splice; + + if (!type.Array(o[lastKey])) + throw makeError(path, 'using command $push to a non array'); - // Logging update - log[h] = true; - o[k] = helpers.shallowMerge(o[k], v); + v.forEach(function(args) { + o[lastKey] = helpers.splice.apply(null, [o[lastKey]].concat(args)); + }); } - else if (opts.shiftReferences && - ('$push' in (spec[k] || {}) || - '$unshift' in (spec[k] || {}))) { - if ('$push' in (spec[k] || {})) { - v = spec[k].$push; - - if (!type.Array(o[k])) - 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 (!type.Array(o[k])) - 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; + + // $push + if (k === '$push') { + v = spec.$push; + + if (!type.Array(o[lastKey])) + throw makeError(path, 'using command $push to a non array'); + + o[lastKey] = o[lastKey].concat(v); } - else { - - // If nested object does not exist, we create it - if (typeof o[k] === 'undefined') - o[k] = {}; - - // Shifting reference - if (opts.shiftReferences) - o[k] = helpers.shallowClone(o[k]); - - // Recur - // TODO: fix this horrendous behaviour. - mutator( - o[k], - spec[k], - path.concat(k), - o - ); + + // $unshift + if (k === '$unshift') { + v = spec.$unshift; + + if (!type.Array(o[lastKey])) + throw makeError(path, 'using command $unshift to a non array'); + + o[lastKey] = [].concat(v).concat(o[lastKey]); } } } - })(target, spec); + else { + for (k in spec) { + + // If nested object does not exist, we create it + if (typeof o[lastKey][k] === 'undefined') + o[lastKey][k] = {}; + else + o[lastKey][k] = helpers.shallowClone(o[lastKey][k]); + + // Recur + mutator( + o[lastKey], + spec[k], + path.concat(k), + o + ); + } + } + }; - return Object.keys(log).map(function(hash) { - return hash.split('λ'); - }); -} + mutator(data, spec); + + // Returning data and path log + return { + data: data.root, -// Exporting -module.exports = update; + // SHIFT LOG + log: Object.keys(log).map(function(hash) { + return hash.split('|λ|').slice(1); + }) + }; +}; diff --git a/test/endpoint.js b/test/endpoint.js index 334ba01..06b133f 100644 --- a/test/endpoint.js +++ b/test/endpoint.js @@ -7,5 +7,3 @@ require('./suites/helpers.js'); require('./suites/baobab.js'); require('./suites/cursor.js'); -require('./suites/combination.js'); -require('./suites/mixins.js'); diff --git a/test/state.js b/test/state.js index 62fec29..061a996 100644 --- a/test/state.js +++ b/test/state.js @@ -13,6 +13,7 @@ module.exports = { firstname: 'John', lastname: 'Dillinger' }, + pointer: 1, setLater: null, list: [[1, 2], [3, 4]], longList: [1, 2, 3, 4], diff --git a/test/suites/baobab.js b/test/suites/baobab.js index 2030a87..f209d22 100644 --- a/test/suites/baobab.js +++ b/test/suites/baobab.js @@ -4,79 +4,72 @@ */ var assert = require('assert'), state = require('../state.js'), - Typology = require('typology'), Baobab = require('../../src/baobab.js'), Cursor = require('../../src/cursor.js'), + Facet = require('../../src/facet.js'), async = require('async'), - clone = require('lodash.clonedeep'); + _ = require('lodash'); describe('Baobab API', function() { describe('Basics', function() { var baobab = new Baobab(state); - it('should be possible to retrieve full data.', function() { - var data = baobab.get(); - assert.deepEqual(data, state); + it('should throw an error when trying to instantiate an baobab with incorrect data.', function() { + assert.throws(function() { + new Baobab(undefined); + }, /invalid data/); }); - it('should be possible to retrieve nested data.', function() { - var colors = baobab.get(['one', 'subtwo', 'colors']); - assert.deepEqual(colors, state.one.subtwo.colors); + it('should be possible to instantiate without the "new" keyword.', function() { + var special = Baobab(state); - // Polymorphism - var primitive = baobab.get('primitive'); - assert.strictEqual(primitive, 3); + assert(special.get('two'), baobab.get('two')); }); + }); - it('should be possible to get data from both maps and lists.', function() { - var yellow = baobab.get(['one', 'subtwo', 'colors', 1]); + describe('Selection', function() { + var baobab = new Baobab(state); - assert.strictEqual(yellow, 'yellow'); + it('selecting data in the baobab should return a cursor.', function() { + assert(baobab.select(['one']) instanceof Cursor); }); - it('should return undefined when data is not to be found through path.', function() { - var inexistant = baobab.get(['no']); - assert.strictEqual(inexistant, undefined); + it('should be possible to use some polymorphism on the selection.', function() { + var altCursor = baobab.select('one', 'subtwo', 'colors'); - // Nesting - var nestedInexistant = baobab.get(['no', 'no']); - assert.strictEqual(nestedInexistant, undefined); + assert.deepEqual(altCursor.get(), state.one.subtwo.colors); }); - it('should be possible to retrieve items with a function in path.', function() { - var yellow = baobab.get('one', 'subtwo', 'colors', function(i) { return i === 'yellow'; }); + it('should be possible to select data using a function.', function() { + var cursor = baobab.select('one', 'subtwo', 'colors', function(v) { + return v === 'yellow'; + }); - assert.strictEqual(yellow, 'yellow'); + assert.strictEqual(cursor.get(), 'yellow'); }); - it('should be possible to retrieve items with a descriptor object.', function() { - var firstItem = baobab.get('items', {id: 'one'}), - secondItem = baobab.get('items', {id: 'two', user: {name: 'John'}}), - thirdItem = baobab.get('items', {id: ['one', 'two']}); + it('should be possible to select data using a descriptor object.', function() { + var cursor = baobab.select('items', {id: 'one'}); - assert.deepEqual(firstItem, {id: 'one'}); - assert.deepEqual(secondItem, {id: 'two', user: {name: 'John', surname: 'Talbot'}}); - assert.deepEqual(firstItem, {id: 'one'}); + assert.deepEqual(cursor.get(), {id: 'one'}); }); - it('should not fail when retrieved data is null on the path.', function() { - var nullValue = baobab.get('setLater'); - assert.strictEqual(nullValue, null); + it('should be possible to select data using a cursor pointer.', function() { + var cursor = baobab.select('one', 'subtwo', 'colors', {$cursor: ['pointer']}); - var inexistant = baobab.get('setLater', 'a'); - assert.strictEqual(inexistant, undefined); + assert.strictEqual(cursor.get(), 'yellow'); }); - it('should throw an error when trying to instantiate an baobab with incorrect data.', function() { - assert.throws(function() { - new Baobab(undefined); - }, /invalid data/); - }); + it('should fail when providing a wrong path to the $cursor command.', function() { + assert.throws(function() { + var color = baobab.select('one', 'subtwo', 'colors', {$cursor: null}); + }, /\$cursor/); + }); + }); - it('selecting data in the baobab should return a cursor.', function() { - assert(baobab.select(['one']) instanceof Cursor); - }); + describe('Events', function() { + var baobab = new Baobab(state); it('should be possible to listen to update events.', function(done) { baobab.on('update', function(e) { @@ -103,44 +96,6 @@ describe('Baobab API', function() { done(); }, 30); }); - - it('should be possible to instantiate without the "new" keyword.', function() { - var special = Baobab(state); - - assert(special.get('two'), baobab.get('two')); - }); - }); - - describe('Updates', function() { - - it('should be possible to set a key using a path rather than a key.', function() { - var baobab = new Baobab(state, {asynchronous: false}); - - baobab.set(['two', 'age'], 34); - assert.strictEqual(baobab.get().two.age, 34); - }); - - it('should be possible to set a key at an nonexistent path.', function() { - var baobab = new Baobab(state, {asynchronous: false}); - - baobab.set(['nonexistent', 'key'], 'hello'); - assert.strictEqual(baobab.get().nonexistent.key, 'hello'); - }); - - it('should be possible to set a key using a dynamic path.', function() { - var baobab = new Baobab(state, {asynchronous: false}); - - baobab.set(['items', {id: 'two'}, 'user', 'age'], 34); - assert.strictEqual(baobab.get().items[1].user.age, 34); - }); - - it('should fail when setting a nonexistent dynamic path.', function() { - var baobab = new Baobab(state, {asynchronous: false}); - - assert.throws(function() { - baobab.set(['items', {id: 'four'}, 'user', 'age'], 34); - }, /solve/); - }); }); describe('Advanced', function() { @@ -159,177 +114,357 @@ describe('Baobab API', function() { assert(baobab.data === undefined); }); - }); - describe('Options', function() { - it('should be possible to commit changes immediately.', function() { - var baobab = new Baobab({hello: 'world'}, {asynchronous: false}); - baobab.set('hello', 'you'); - assert.strictEqual(baobab.get('hello'), 'you'); + it('the tree should shift references on updates.', function() { + var list = [1], + baobab = new Baobab({list: list}, {asynchronous: false}); + + baobab.select('list').push(2); + assert.deepEqual(baobab.get('list'), [1, 2]); + assert(list !== baobab.get('list')); }); - it('should be possible to let the user commit himself.', function(done) { - var baobab = new Baobab({number: 1}, {autoCommit: false}); - baobab.set('number', 2); + it('the tree should also shift parent references.', function() { + var shiftingTree = new Baobab({root: {admin: {items: [1], other: [2]}}}, {asynchronous: false}); - setTimeout(function() { - assert.strictEqual(baobab.get('number'), 1); - baobab.commit(); - setTimeout(function() { - assert.strictEqual(baobab.get('number'), 2); - done(); - }, 0); - }, 0); - }); + var shiftingOriginal = shiftingTree.get(); + + shiftingTree.select('root', 'admin', 'items').push(2); - it('should be possible to serve cloned data.', function() { - var baobab1 = new Baobab({hello: 'world'}), - baobab2 = new Baobab({hello: 'world'}, {clone: true}); + assert.deepEqual(shiftingTree.get('root', 'admin', 'items'), [1, 2]); - assert(baobab1.get() === baobab1.data); - assert(baobab1.clone() !== baobab1.data); - assert.deepEqual(baobab1.clone(), baobab1.data); - assert(baobab2.get() !== baobab2.data); - assert(baobab2.reference() === baobab2.data); - assert.deepEqual(baobab2.get(), {hello: 'world'}); + assert(shiftingTree.get() !== shiftingOriginal); + assert(shiftingTree.get().root !== shiftingOriginal.root); + assert(shiftingTree.get().root.admin !== shiftingOriginal.root.admin); + assert(shiftingTree.get().root.admin.items !== shiftingOriginal.root.admin.items); + assert(shiftingTree.get().root.admin.other === shiftingOriginal.root.admin.other); }); + }); - it('should be possible to shunt the singleton cursors.', function() { - var baobab1 = new Baobab({hello: 'world'}), - baobab2 = new Baobab({hello: 'world'}, {cursorSingletons: false}); + describe('Facets', function() { + var baobab = new Baobab( - assert(baobab1.select('hello') === baobab1.select('hello')); - assert(baobab2.select('hello') !== baobab2.select('hello')); - }); + // Data + { + projects: [ + { + id: 1, + name: 'Tezcatlipoca', + user: 'John' + }, + { + id: 2, + name: 'Huitzilopochtli', + user: 'John' + }, + { + id: 3, + name: 'Tlaloc', + user: 'Jack' + } + ], + currentProjectId: 1, + value1: 'Hello', + value2: 'World' + }, + + // Options + { + asynchronous: false, + facets: { + filtered: { + cursors: { + projects: ['projects'] + }, + get: function(data) { + return data.projects.filter(function(p) { + return p.user === 'John'; + }); + } + }, + current: { + cursors: { + id: ['currentProjectId'], + projects: ['projects'] + }, + get: function(data) { + return _.find(data.projects, {id: data.id}); + } + } + } + } + ); - it('should be possible to provide your own cloning function to the tree.', function() { - var baobab = new Baobab({hello: 'world'}, {cloningFunction: clone}); + var filtered = baobab.facets.filtered, + current = baobab.facets.current; - assert(baobab._cloner === clone); - assert.deepEqual(baobab.clone(), baobab.data); + it('baobab.createFacet should return a facet instance.', function() { + var facet = baobab.createFacet({cursors: {list: ['list']}}); + assert(facet instanceof Facet); + facet.release(); }); - it('should be possible to tell the tree to shift references on updates.', function() { - var list = [1], - baobab = new Baobab({list: list}, {shiftReferences: true, asynchronous: false}); + it('should fail when creating a facet from incorrect mappings.', function() { - baobab.select('list').push(2); - assert.deepEqual(baobab.get('list'), [1, 2]); - assert(list !== baobab.get('list')); + assert.throws(function() { + baobab.createFacet({cursors: ['wrong']}); + }, /mapping/); }); - it('should also shift parent references.', function() { - var tree = new Baobab({root: {admin: {items: [1], other: [2]}}}, {asynchronous: false}), - shiftingTree = new Baobab({root: {admin: {items: [1], other: [2]}}}, {shiftReferences: true, asynchronous: false}); + it('should fire correctly.', function() { + var tree = new Baobab({list: [1, 2, 3], otherlist: [4, 5, 6], unrelated: 0}, {autoCommit: false}), + list = tree.select('list'), + other = tree.select('otherlist'), + unrelated = tree.select('unrelated'); - var original = tree.reference(), - shiftingOriginal = shiftingTree.reference(); + var count = 0, + inc = function() {count++;}; - tree.select('root', 'admin', 'items').push(2); - shiftingTree.select('root', 'admin', 'items').push(2); + var facet = tree.createFacet({cursors: {list: ['list'], otherList: ['otherlist']}}); + facet.on('update', inc); + + list.push(4); + tree.commit(); + + assert.strictEqual(count, 1); + + unrelated.set(1); + tree.commit(); - assert.deepEqual(tree.reference('root', 'admin', 'items'), [1, 2]); - assert.deepEqual(shiftingTree.reference('root', 'admin', 'items'), [1, 2]); + assert.strictEqual(count, 1); - assert(tree.reference() === original); - assert(tree.reference().root === original.root); - assert(tree.reference().root.admin === original.root.admin); - assert(tree.reference().root.admin.items === original.root.admin.items); - assert(tree.reference().root.admin.other === original.root.admin.other); + other.push(4); + tree.commit(); - assert(shiftingTree.reference() !== shiftingOriginal); - assert(shiftingTree.reference().root !== shiftingOriginal.root); - assert(shiftingTree.reference().root.admin !== shiftingOriginal.root.admin); - assert(shiftingTree.reference().root.admin.items !== shiftingOriginal.root.admin.items); - assert(shiftingTree.reference().root.admin.other === shiftingOriginal.root.admin.other); + assert.strictEqual(count, 2); + + list.push(5); + other.push(5); + tree.commit(); + + assert.strictEqual(count, 3); + + facet.release(); }); - }); - describe('Custom typology', function() { + it('should be possible to get data from facets.', function() { + assert.deepEqual(filtered.get(), [ + { + id: 1, + name: 'Tezcatlipoca', + user: 'John' + }, + { + id: 2, + name: 'Huitzilopochtli', + user: 'John' + } + ]); + + assert.deepEqual(current.get(), { + id: 1, + name: 'Tezcatlipoca', + user: 'John' + }); + + baobab.update({ + projects: { + $push: { + id: 4, + name: 'Coatlicue', + user: 'John' + } + }, + currentProjectId: { + $set: 2 + } + }); + + assert.deepEqual(filtered.get(), [ + { + id: 1, + name: 'Tezcatlipoca', + user: 'John' + }, + { + id: 2, + name: 'Huitzilopochtli', + user: 'John' + }, + { + id: 4, + name: 'Coatlicue', + user: 'John' + } + ]); - it('a baobab should have an internal typology.', function() { - var baobab = new Baobab({hello: 'world'}); - assert(baobab.typology instanceof Typology); + assert.deepEqual(current.get(), { + id: 2, + name: 'Huitzilopochtli', + user: 'John' + }); }); - it('should be possible to pass a custom typology at instantiation.', function() { - var typology = new Typology({user: '?object'}), - baobab = new Baobab({hello: 'world'}, {typology: typology}); + it('should be possible to listen to facets.', function() { + var countF = 0, + countC = 0; + + var incF = function() {countF++;}, + incC = function() {countC++;}; + + filtered.on('update', incF); + current.on('update', incC); - assert(baobab.typology instanceof Typology); - assert(baobab.typology.isValid('user')); + baobab.select('projects').push({id: 4, name: 'Coatlicue', user: 'John'}); + baobab.set('currentProjectId', 2); + + assert.strictEqual(countF, 1); + assert.strictEqual(countC, 2); }); - it('should be possible to pass an object defining custom types at instantiation.', function() { - var definitions = {user: '?object'}, - baobab = new Baobab({hello: 'world'}, {typology: definitions}); + it('should be possible to pass cursors directly to facets.', function() { + var cursor = baobab.select('value1'); - assert(baobab.typology instanceof Typology); - assert(baobab.typology.isValid('user')); + var facet = baobab.createFacet({ + cursors: { + value1: cursor, + value2: ['value2'] + } + }); + + assert.deepEqual(facet.get(), { + value1: 'Hello', + value2: 'World' + }); + + facet.release(); }); - it('invalid initial data should throw an error.', function() { + it('should be possible to solve cursors mapping with a function.', function() { + var pointer = 'value1'; - assert.throws(function() { - new Baobab( - {hello: 'world'}, - { - validate: {hello: 'word'}, - typology: {word: 'object'} - } - ); - }, /Expected/); + var facet = baobab.createFacet({ + cursors: function() { + return {value: [pointer]}; + } + }); + + assert.deepEqual(facet.get(), {value: 'Hello'}); + + pointer = 'value2'; + facet.refresh(); + + assert.deepEqual(facet.get(), {value: 'World'}); + + facet.release(); }); - it('should emit an "invalid" event when data validation fails on commit.', function(done) { - var baobab = new Baobab({hello: 'world'}, {validate: {hello: 'string'}}); + it('should be possible to base facets on other facets, yo dawg.', function() { + var facet = baobab.createFacet({facets: {value: 'current'}}), + value2 = baobab.createFacet({cursors: {value: 'value2'}}); - baobab.on('invalid', function(e) { - done(); + assert.deepEqual(facet.get(), { + value: { + id: 2, + name: 'Huitzilopochtli', + user: 'John' + } + }); + + facet.release(); + + facet = baobab.createFacet({facets: {value2: value2}, cursors: {value1: ['value1']}}); + + assert.deepEqual(facet.get(), { + value1: 'Hello', + value2: { + value: 'World' + } }); - baobab.set('hello', 42); + + facet.release(); }); }); - describe('History', function() { - - it('should be possible to record passed states.', function(done) { - var baobab = new Baobab({name: 'Maria'}, {maxHistory: 1}); + describe('Options', function() { + it('should be possible to commit changes immediately.', function() { + var baobab = new Baobab({hello: 'world'}, {asynchronous: false}); + baobab.set('hello', 'you'); + assert.strictEqual(baobab.get('hello'), 'you'); + }); - baobab.set('name', 'Estelle'); + it('should be possible to let the user commit himself.', function(done) { + var baobab = new Baobab({number: 1}, {autoCommit: false}); + baobab.set('number', 2); setTimeout(function() { - assert(baobab.hasHistory()); - assert.deepEqual(baobab.getHistory(), [{log: [['name']], data: {name: 'Maria'}}]); - done(); + assert.strictEqual(baobab.get('number'), 1); + baobab.commit(); + setTimeout(function() { + assert.strictEqual(baobab.get('number'), 2); + done(); + }, 0); }, 0); }); - it('should throw an error if trying to undo without history.', function() { - var baobab = new Baobab({hello: 'world'}); + it('should be possible to validate the tree and rollback on fail.', function() { + var invalidCount = 0; - assert.throws(function() { - baobab.undo(); - }, /no history/); + function v(state, nextState, paths) { + assert(this === baobab); + + if (typeof nextState.hello !== 'string') + return new Error('Invalid tree!'); + } + + var baobab = new Baobab({hello: 'world'}, {validate: v, asynchronous: false}); + + baobab.on('invalid', function(e) { + var error = e.data.error; + + assert.strictEqual(error.message, 'Invalid tree!'); + invalidCount++; + }); + + baobab.set('hello', 'John'); + + assert.strictEqual(invalidCount, 0); + assert.strictEqual(baobab.get('hello'), 'John'); + + baobab.set('hello', 4); + + assert.strictEqual(invalidCount, 1); + assert.strictEqual(baobab.get('hello'), 'John'); }); - it('should be possible to go back in time.', function(done) { - var baobab = new Baobab({name: 'Maria'}, {maxHistory: 2}); + it('should be possible to validate the tree and let the tree update on fail.', function() { + var invalidCount = 0; - async.series([ - function(next) { - baobab.set('name', 'Estelle'); - setTimeout(next); - }, - function(next) { - assert(baobab.hasHistory()); - baobab.undo(); - assert.deepEqual(baobab.get(), {name: 'Maria'}); - done(); - } - ], function() { - done(); + function v(state, nextState, paths) { + assert(this === baobab); + + if (typeof nextState.hello !== 'string') + return new Error('Invalid tree!'); + } + + var baobab = new Baobab({hello: 'world'}, {validate: v, asynchronous: false, validationBehavior: 'notify'}); + + baobab.on('invalid', function(e) { + var error = e.data.error; + + assert.strictEqual(error.message, 'Invalid tree!'); + invalidCount++; }); + + baobab.set('hello', 'John'); + + assert.strictEqual(invalidCount, 0); + assert.strictEqual(baobab.get('hello'), 'John'); + + baobab.set('hello', 4); + + assert.strictEqual(invalidCount, 1); + assert.strictEqual(baobab.get('hello'), 4); }); }); }); diff --git a/test/suites/combination.js b/test/suites/combination.js deleted file mode 100644 index 8c778d8..0000000 --- a/test/suites/combination.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Baobab Helpers Unit Tests - * ========================== - */ -var assert = require('assert'), - state = require('../state.js'), - Baobab = require('../../src/baobab.js'), - Combination = require('../../src/combination.js'); - -describe('Combination', function() { - var baobab = new Baobab({list: [1, 2, 3], otherlist: [4, 5, 6], againList: [7, 8, 9]}, {autoCommit: false}), - cursor = baobab.select('list'), - othercursor = baobab.select('otherlist'), - againCursor = baobab.select('againList'); - - it('related cursor methods should return Combination instances.', function() { - var or = cursor.or(othercursor), - and = cursor.and(othercursor); - - assert(or instanceof Combination); - assert(and instanceof Combination); - - or.release(); - and.release(); - }); - - it('should fail when using combination wrongly.', function() { - assert.throws(function() { - cursor.or(cursor); - }, /already/); - }); - - it('should be possible to listen to "or" combinations.', function() { - var combination = cursor.or(othercursor), - count = 0; - - combination.on('update', function() { - count++; - }); - - // 1 - cursor.push(4); - baobab.commit(); - - // 2 - othercursor.push(7); - baobab.commit(); - - // 3 - cursor.edit([1]); - othercursor.edit([4]); - baobab.commit(); - - assert.strictEqual(count, 3); - combination.release(); - }); - - it('should be possible to listen to "and" combinations.', function() { - var combination = cursor.and(othercursor), - count = 0; - - combination.on('update', function() { - count++; - }); - - // 1 - cursor.push(4); - baobab.commit(); - - // 2 - othercursor.push(7); - baobab.commit(); - - // 3 - cursor.edit([1]); - othercursor.edit([4]); - baobab.commit(); - - assert.strictEqual(count, 1); - combination.release(); - }); - - it('should be possible to make complex "or" combinations.', function() { - var combination = cursor.or(othercursor).or(againCursor), - count = 0; - - combination.on('update', function() { - count++; - }); - - // 1 - cursor.push(4); - baobab.commit(); - - // 2 - othercursor.push(7); - baobab.commit(); - - // 3 - cursor.edit([1]); - othercursor.edit([4]); - baobab.commit(); - - // 4 - againCursor.edit([7]); - baobab.commit(); - - combination.release(); - assert.strictEqual(count, 4); - }); - - it('should be possible to make complex "and" combinations.', function() { - var combination = cursor.and(othercursor).and(againCursor), - count = 0; - - combination.on('update', function() { - count++; - }); - - // 1 - cursor.push(4); - baobab.commit(); - - // 2 - othercursor.push(7); - baobab.commit(); - - // 3 - cursor.edit([1]); - othercursor.edit([4]); - baobab.commit(); - - // 4 - againCursor.edit([7]); - baobab.commit(); - - // 5 - cursor.edit([1]); - othercursor.edit([4]); - againCursor.edit([7]); - baobab.commit(); - - assert.strictEqual(count, 1); - combination.release(); - }); - - it('should be possible to mix combinations.', function() { - var combination = cursor.or(othercursor).and(againCursor), - count = 0; - - combination.on('update', function() { - count++; - }); - - // 1 - cursor.edit([1]); - againCursor.edit([7]); - baobab.commit(); - - // 2 - againCursor.edit([7]); - baobab.commit(); - - // 3 - cursor.edit([1]); - othercursor.edit([4]); - baobab.commit(); - - assert.strictEqual(count, 1); - combination.release(); - }); - - it('should be possible to use some polymorphism.', function() { - - // First case - var combination = new Combination('or', [cursor, othercursor]), - count = 0; - - combination.on('update', function() { - count++; - }); - - // 1 - cursor.push(4); - baobab.commit(); - - // 2 - othercursor.push(7); - baobab.commit(); - - // 3 - cursor.edit([1]); - othercursor.edit([4]); - baobab.commit(); - - assert.strictEqual(count, 3); - combination.release(); - - // Second case - combination = new Combination('or', cursor, othercursor); - count = 0; - - combination.on('update', function() { - count++; - }); - - // 1 - cursor.push(4); - baobab.commit(); - - // 2 - othercursor.push(7); - baobab.commit(); - - // 3 - cursor.edit([1]); - othercursor.edit([4]); - baobab.commit(); - - assert.strictEqual(count, 3); - combination.release(); - }); - - it('a single cursor combination should work as expected.', function() { - var combination = new Combination('or', cursor), - count = 0; - - combination.on('update', function() { - count++; - }); - - // 1 - cursor.push(4); - baobab.commit(); - - // 2 - cursor.push(5); - baobab.commit(); - - assert.strictEqual(count, 2); - combination.release(); - }); -}); diff --git a/test/suites/cursor.js b/test/suites/cursor.js index afb0d75..22b6aa6 100644 --- a/test/suites/cursor.js +++ b/test/suites/cursor.js @@ -5,282 +5,292 @@ var assert = require('assert'), state = require('../state.js'), helpers = require('../../src/helpers.js'), - Typology = require('typology'), Baobab = require('../../src/baobab.js'), async = require('async'); describe('Cursor API', function() { - describe('Basics', function() { + describe('Getters', function() { var baobab = new Baobab(state); - var colorCursor = baobab.select(['one', 'subtwo', 'colors']), - oneCursor = baobab.select('one'); + describe('Root cursor', function() { + it('should be possible to retrieve full data.', function() { + var data = baobab.get(); + assert.deepEqual(data, state); + }); - it('should be possible to retrieve data at cursor.', function() { - var colors = colorCursor.get(); + it('should be possible to retrieve nested data.', function() { + var colors = baobab.get(['one', 'subtwo', 'colors']); + assert.deepEqual(colors, state.one.subtwo.colors); - assert(colors instanceof Array); - assert.deepEqual(colors, state.one.subtwo.colors); - }); + // Polymorphism + var primitive = baobab.get('primitive'); + assert.strictEqual(primitive, 3); + }); - it('should be possible to retrieve data with a 0 key.', function() { - var sub = new Baobab([1, 2]); - assert.strictEqual(sub.get(0), 1); - assert.strictEqual(colorCursor.get(0), 'blue'); - }); + it('should be possible to get data from both maps and lists.', function() { + var yellow = baobab.get(['one', 'subtwo', 'colors', 1]); - it('should be possible to retrieve nested data.', function() { - var colors = oneCursor.get(['subtwo', 'colors']); + assert.strictEqual(yellow, 'yellow'); + }); - assert.deepEqual(colors, state.one.subtwo.colors); - }); + it('should return undefined when data is not to be found through path.', function() { + var inexistant = baobab.get(['no']); + assert.strictEqual(inexistant, undefined); - it('should be possible to use some polymorphism on the selection.', function() { - var altCursor = baobab.select('one', 'subtwo', 'colors'); + // Nesting + var nestedInexistant = baobab.get(['no', 'no']); + assert.strictEqual(nestedInexistant, undefined); + }); - assert.deepEqual(altCursor.get(), colorCursor.get()); - }); + it('should be possible to retrieve items with a function in path.', function() { + var yellow = baobab.get('one', 'subtwo', 'colors', function(i) { return i === 'yellow'; }); - it('should be possible to select data using a function.', function() { - var cursor = baobab.select('one', 'subtwo', 'colors', function(v) { - return v === 'yellow'; + assert.strictEqual(yellow, 'yellow'); }); - assert.strictEqual(cursor.get(), 'yellow'); - }); + it('should be possible to retrieve items with a descriptor object.', function() { + var firstItem = baobab.get('items', {id: 'one'}), + secondItem = baobab.get('items', {id: 'two', user: {name: 'John'}}), + thirdItem = baobab.get('items', {id: ['one', 'two']}); - it('should be possible to select data using a descriptor object.', function() { - var cursor = baobab.select('items', {id: 'one'}); + assert.deepEqual(firstItem, {id: 'one'}); + assert.deepEqual(secondItem, {id: 'two', user: {name: 'John', surname: 'Talbot'}}); + assert.deepEqual(firstItem, {id: 'one'}); + }); - assert.deepEqual(cursor.get(), {id: 'one'}); - }); + it('should not fail when retrieved data is null on the path.', function() { + var nullValue = baobab.get('setLater'); + assert.strictEqual(nullValue, null); - it('should be possible to use some polymorphism on the getter.', function() { - var altCursor = baobab.select('one'); + var inexistant = baobab.get('setLater', 'a'); + assert.strictEqual(inexistant, undefined); + }); - assert.deepEqual(altCursor.get('subtwo', 'colors'), state.one.subtwo.colors); - }); + it('should be able to resolve a cursor pointer.', function() { + var color = baobab.get('one', 'subtwo', 'colors', {$cursor: ['pointer']}); - it('should be possible to listen to updates.', function(done) { - colorCursor.on('update', function() { - assert.deepEqual(colorCursor.get(), ['blue', 'yellow', 'purple']); - done(); + assert.strictEqual(color, 'yellow'); }); - colorCursor.push('purple'); + it('should fail when providing a wrong path to the $cursor command.', function() { + assert.throws(function() { + var color = baobab.get('one', 'subtwo', 'colors', {$cursor: null}); + }, /\$cursor/); + }); }); - }); - describe('Predicates', function() { - var baobab = new Baobab(state); - - it('should be possible to tell whether cursor is root.', function() { - assert(baobab.select('one').up().isRoot()); - assert(!baobab.select('one').isRoot()); - }); + describe('Standard cursors', function() { + var colorCursor = baobab.select(['one', 'subtwo', 'colors']), + oneCursor = baobab.select('one'); - it('should be possible to tell whether cursor is leaf.', function() { - assert(baobab.select('primitive').isLeaf()); - assert(!baobab.select('one').isLeaf()); - }); + it('should be possible to retrieve data at cursor.', function() { + var colors = colorCursor.get(); - it('should be possible to tell whether cursor is branch.', function() { - assert(baobab.select('one').isBranch()); - assert(!baobab.select('one').up().isBranch()); - assert(!baobab.select('primitive').isBranch()); - }); - }); - - describe('Updates', function() { - it('should be possible to set a key using a path rather than a key.', function() { - var baobab = new Baobab(state, {asynchronous: false}), - cursor = baobab.select('items'); + assert(colors instanceof Array); + assert.deepEqual(colors, state.one.subtwo.colors); + }); - cursor.set([1, 'user', 'age'], 34); - assert.strictEqual(cursor.get()[1].user.age, 34); - }); + it('should be possible to retrieve data with a 0 key.', function() { + var sub = new Baobab([1, 2]); + assert.strictEqual(sub.get(0), 1); + assert.strictEqual(colorCursor.get(0), 'blue'); + }); - it('should be possible to set a key at an nonexistent path.', function() { - var baobab = new Baobab(state, {asynchronous: false}), - cursor = baobab.select('two'); + it('should be possible to retrieve nested data.', function() { + var colors = oneCursor.get(['subtwo', 'colors']); - cursor.set(['nonexistent', 'key'], 'hello'); - assert.strictEqual(cursor.get().nonexistent.key, 'hello'); - }); + assert.deepEqual(colors, state.one.subtwo.colors); + }); - it('should be possible to set a key using a dynamic path.', function() { - var baobab = new Baobab(state, {asynchronous: false}), - cursor = baobab.select('items'); + it('should be possible to use some polymorphism on the getter.', function() { + var altCursor = baobab.select('one'); - cursor.set([{id: 'two'}, 'user', 'age'], 34); - assert.strictEqual(cursor.get()[1].user.age, 34); + assert.deepEqual(altCursor.get('subtwo', 'colors'), state.one.subtwo.colors); + }); }); + }); - it('should fail when setting a nonexistent dynamic path.', function() { - var baobab = new Baobab(state, {asynchronous: false}), - cursor = baobab.select('items'); + describe('Setters', function() { - assert.throws(function() { - cursor.set([{id: 'four'}, 'user', 'age'], 34); - }, /solve/); - }); + describe('Root cursor', function() { + it('should be possible to set a key using a path rather than a key.', function() { + var baobab = new Baobab(state, {asynchronous: false}); - it('should throw an error when trying to push to a non-array.', function() { - var baobab = new Baobab(state), - oneCursor = baobab.select('one'); + baobab.set(['two', 'age'], 34); + assert.strictEqual(baobab.get().two.age, 34); + }); - assert.throws(function() { - oneCursor.push('test'); - }, /non-array/); - }); + it('should be possible to set a key at an nonexistent path.', function() { + var baobab = new Baobab(state, {asynchronous: false}); - it('should throw an error when trying to unshift to a non-array.', function() { - var baobab = new Baobab(state), - oneCursor = baobab.select('one'); + baobab.set(['nonexistent', 'key'], 'hello'); + assert.strictEqual(baobab.get().nonexistent.key, 'hello'); + }); - assert.throws(function() { - oneCursor.unshift('test'); - }, /non-array/); - }); + it('should be possible to set a key using a dynamic path.', function() { + var baobab = new Baobab(state, {asynchronous: false}); - it('should be possible to chain mutations.', function(done) { - var baobab = new Baobab({number: 1}), - inc = function(i) { return i + 1; }; + baobab.set(['items', {id: 'two'}, 'user', 'age'], 34); + assert.strictEqual(baobab.get().items[1].user.age, 34); + }); - baobab.update({number: {$chain: inc}}); - baobab.update({number: {$chain: inc}}); + it('should fail when setting a nonexistent dynamic path.', function() { + var baobab = new Baobab(state, {asynchronous: false}); - baobab.on('update', function() { - assert.strictEqual(baobab.get('number'), 3); - done(); + assert.throws(function() { + baobab.set(['items', {id: 'four'}, 'user', 'age'], 34); + }, /solve/); }); }); - it('a single $chain command should work like an $apply.', function() { - var baobab = new Baobab({number: 1}, {asynchronous: false}), - cursor = baobab.select('number'), - inc = function(i) { return i + 1; }; + describe('Standard cursors', function() { + it('should warn the user when too many arguments are applied to a setter.', function() { + var baobab = new Baobab(state), + cursor = baobab.select('items'); - assert.strictEqual(cursor.get(), 1); - cursor.chain(inc); - assert.strictEqual(cursor.get(), 2); - }); + assert.throws(function() { + cursor.set('this', 'is', 'my', 'destiny!'); + }, /too many/); + }); - it('should be possible to shallow merge two objects.', function(done) { - var baobab = new Baobab({o: {hello: 'world'}, string: 'test'}); + it('should be possible to set a key using a path rather than a key.', function() { + var baobab = new Baobab(state, {asynchronous: false}), + cursor = baobab.select('items'); - assert.throws(function() { - baobab.select('test').merge({hello: 'moto'}); - }, /merge/); + cursor.set([1, 'user', 'age'], 34); + assert.strictEqual(cursor.get()[1].user.age, 34); + }); - var cursor = baobab.select('o'); - cursor.merge({hello: 'jarl'}); + it('should be possible to set a key at an nonexistent path.', function() { + var baobab = new Baobab(state, {asynchronous: false}), + cursor = baobab.select('two'); - baobab.on('update', function() { - assert.deepEqual(baobab.get('o'), {hello: 'jarl'}); - done(); + cursor.set(['nonexistent', 'key'], 'hello'); + assert.strictEqual(cursor.get().nonexistent.key, 'hello'); }); - }); - it('should be possible to remove keys from the tree.', function() { - var tree = new Baobab({one: 1, two: 2}, {asynchronous: false}); + it('should be possible to set a key using a dynamic path.', function() { + var baobab = new Baobab(state, {asynchronous: false}), + cursor = baobab.select('items'); - assert.deepEqual(tree.get(), {one: 1, two: 2}); - tree.unset('one'); - assert.deepEqual(tree.get(), {two: 2}); - }); + cursor.set([{id: 'two'}, 'user', 'age'], 34); + assert.strictEqual(cursor.get()[1].user.age, 34); + }); - it('should be possible to remove keys from a cursor.', function() { - var tree = new Baobab({one: 1, two: {subone: 1, subtwo: 2}}, {asynchronous: false}), - cursor = tree.select('two'); + it('should fail when setting a nonexistent dynamic path.', function() { + var baobab = new Baobab(state, {asynchronous: false}), + cursor = baobab.select('items'); - assert.deepEqual(cursor.get(), {subone: 1, subtwo: 2}); - cursor.unset('subone'); - assert.deepEqual(cursor.get(), {subtwo: 2}); - }); + assert.throws(function() { + cursor.set([{id: 'four'}, 'user', 'age'], 34); + }, /solve/); + }); - it('should be possible to remove data at cursor.', function() { - var tree = new Baobab({one: 1, two: {subone: 1, subtwo: 2}}, {asynchronous: false}), - cursor = tree.select('two'); + it('should be possible to chain mutations.', function(done) { + var baobab = new Baobab({number: 1}), + inc = function(i) { return i + 1; }; - assert.deepEqual(cursor.get(), {subone: 1, subtwo: 2}); - cursor.remove(); - assert.strictEqual(cursor.get(), undefined); - }); - }); + baobab.update({number: {$chain: inc}}); + baobab.update({number: {$chain: inc}}); - describe('Traversal', function() { - var baobab = new Baobab(state); + baobab.on('update', function() { + assert.strictEqual(baobab.get('number'), 3); + done(); + }); + }); - var colorCursor = baobab.select(['one', 'subtwo', 'colors']), - oneCursor = baobab.select('one'); + it('a single $chain command should work like an $apply.', function() { + var baobab = new Baobab({number: 1}, {asynchronous: false}), + cursor = baobab.select('number'), + inc = function(i) { return i + 1; }; - it('should be possible to create subcursors.', function() { - var sub = oneCursor.select(['subtwo', 'colors']); - assert.deepEqual(sub.get(), state.one.subtwo.colors); - }); + assert.strictEqual(cursor.get(), 1); + cursor.chain(inc); + assert.strictEqual(cursor.get(), 2); + }); - it('should be possible to go up.', function() { - var parent = colorCursor.up(); - assert.deepEqual(parent.get(), state.one.subtwo); - }); + it('should be possible to shallow merge two objects.', function(done) { + var baobab = new Baobab({o: {hello: 'world'}, string: 'test'}); - it('a cusor going up to root cannot go higher and returns null.', function() { - var up = baobab.select('one').up(), - upper = up.up(); + var cursor = baobab.select('o'); + cursor.merge({hello: 'jarl'}); - assert.strictEqual(upper, null); - }); + baobab.on('update', function() { + assert.deepEqual(baobab.get('o'), {hello: 'jarl'}); + done(); + }); + }); - it('should be possible to go left.', function() { - var left = colorCursor.select(1).left(); + it('should be possible to remove keys from a cursor.', function() { + var tree = new Baobab({one: 1, two: {subone: 1, subtwo: 2}}, {asynchronous: false}), + cursor = tree.select('two'); - assert.strictEqual(left.get(), 'blue'); - assert.strictEqual(left.left(), null); + assert.deepEqual(cursor.get(), {subone: 1, subtwo: 2}); + cursor.unset('subone'); + assert.deepEqual(cursor.get(), {subtwo: 2}); + }); - assert.throws(function() { - colorCursor.left(); - }, /left/); - }); + it('should be possible to remove data at cursor.', function() { + var tree = new Baobab({one: 1, two: {subone: 1, subtwo: 2}}, {asynchronous: false}), + cursor = tree.select('two'); - it('should be possible to go right.', function() { - var right = colorCursor.select(0).right(); + assert.deepEqual(cursor.get(), {subone: 1, subtwo: 2}); + cursor.unset(); + assert.strictEqual(cursor.get(), undefined); + }); - assert.strictEqual(right.get(), 'yellow'); - assert.strictEqual(right.right(), null); + it('should be possible to splice an array.', function() { + var tree1 = new Baobab({list: [1, 2, 3]}, {asynchronous: false}), + tree2 = new Baobab(tree1.get(), {asynchronous: false}), + cursor1 = tree1.select('list'), + cursor2 = tree2.select('list'); - assert.throws(function() { - colorCursor.right(); - }, /right/); - }); + assert.deepEqual(cursor1.get(), [1, 2, 3]); - it('should be possible to descend.', function() { - var list = baobab.select('list'); + cursor1.splice([[0, 1], [1, 1, 4]]); + cursor2.splice([0, 1]); + cursor2.splice([1, 1, 4]); - assert.deepEqual(list.down().get(), [1, 2]); - assert.strictEqual(colorCursor.down().get(), 'blue'); - assert.strictEqual(colorCursor.down().up().up().select('colors').down().get(), 'blue'); - assert.strictEqual(list.down().right().down().right().get(), 4); - assert.strictEqual(oneCursor.down(), null); - }); + assert.deepEqual(cursor1.get(), [2, 4]); + assert.deepEqual(cursor1.get(), cursor2.get()); + }); - it('should be possible to get to the leftmost item of a list.', function() { - var listItem = baobab.select('longList', 2); + it('should throw errors when updating with wrong values.', function() { + var cursor = (new Baobab()).root; - assert.strictEqual(listItem.get(), 3); - assert.strictEqual(listItem.leftmost().get(), 1); - }); + assert.throws(function() { + cursor.merge('John'); + }, /value/); - it('should be possible to get to the rightmost item of a list.', function() { - var listItem = baobab.select('longList', 2); + assert.throws(function() { + cursor.splice('John'); + }); - assert.strictEqual(listItem.get(), 3); - assert.strictEqual(listItem.rightmost().get(), 4); + assert.throws(function() { + cursor.apply('John'); + }); + + assert.throws(function() { + cursor.chain('John'); + }); + }); }); }); describe('Events', function() { + var baobab = new Baobab(state); + + it('should be possible to listen to updates.', function(done) { + var colorCursor = baobab.select('one', 'subtwo', 'colors'); + + colorCursor.on('update', function() { + assert.deepEqual(colorCursor.get(), ['blue', 'yellow', 'purple']); + done(); + }); + + colorCursor.push('purple'); + }); it('when a parent updates, so does the child.', function(done) { var baobab = new Baobab(state), @@ -308,7 +318,7 @@ describe('Cursor API', function() { done(); }); - parent.edit({firstname: 'Napoleon', lastname: 'Bonaparte'}); + parent.set({firstname: 'Napoleon', lastname: 'Bonaparte'}); }); it('when a child updates, so does the parent.', function(done) { @@ -336,7 +346,7 @@ describe('Cursor API', function() { done(); }); - child.edit('Napoleon'); + child.set('Napoleon'); }); it('when a leave updates, it should not update its siblings.', function(done) { @@ -372,7 +382,7 @@ describe('Cursor API', function() { done(); }); - leaf1.edit('tada'); + leaf1.set('tada'); }); it('should be possible to listen to the cursor\'s relevancy.', function(done) { @@ -414,7 +424,174 @@ describe('Cursor API', function() { done(); }); - cursor.edit('jacky'); + cursor.set('jacky'); + }); + }); + + describe('Predicates', function() { + var baobab = new Baobab(state); + + it('should be possible to tell whether cursor is root.', function() { + assert(baobab.select('one').up().isRoot()); + assert(!baobab.select('one').isRoot()); + }); + + it('should be possible to tell whether cursor is leaf.', function() { + assert(baobab.select('primitive').isLeaf()); + assert(!baobab.select('one').isLeaf()); + }); + + it('should be possible to tell whether cursor is branch.', function() { + assert(baobab.select('one').isBranch()); + assert(!baobab.select('one').up().isBranch()); + assert(!baobab.select('primitive').isBranch()); + }); + }); + + describe('Traversal', function() { + var baobab = new Baobab(state); + + var colorCursor = baobab.select(['one', 'subtwo', 'colors']), + oneCursor = baobab.select('one'); + + it('should be possible to create subcursors.', function() { + var sub = oneCursor.select(['subtwo', 'colors']); + assert.deepEqual(sub.get(), state.one.subtwo.colors); + }); + + it('should be possible to go up.', function() { + var parent = colorCursor.up(); + assert.deepEqual(parent.get(), state.one.subtwo); + }); + + it('a cusor going up to root cannot go higher and returns null.', function() { + var up = baobab.select('one').up(), + upper = up.up(); + + assert.strictEqual(upper, null); + }); + + it('should be possible to go left.', function() { + var left = colorCursor.select(1).left(); + + assert.strictEqual(left.get(), 'blue'); + assert.strictEqual(left.left(), null); + + assert.throws(function() { + colorCursor.left(); + }, /left/); + }); + + it('should be possible to go right.', function() { + var right = colorCursor.select(0).right(); + + assert.strictEqual(right.get(), 'yellow'); + assert.strictEqual(right.right(), null); + + assert.throws(function() { + colorCursor.right(); + }, /right/); + }); + + it('should be possible to descend.', function() { + var list = baobab.select('list'); + + assert.deepEqual(list.down().get(), [1, 2]); + assert.strictEqual(colorCursor.down().get(), 'blue'); + assert.strictEqual(colorCursor.down().up().up().select('colors').down().get(), 'blue'); + assert.strictEqual(list.down().right().down().right().get(), 4); + assert.strictEqual(oneCursor.down(), null); + }); + + it('should be possible to get to the leftmost item of a list.', function() { + var listItem = baobab.select('longList', 2); + + assert.strictEqual(listItem.get(), 3); + assert.strictEqual(listItem.leftmost().get(), 1); + }); + + it('should be possible to get to the rightmost item of a list.', function() { + var listItem = baobab.select('longList', 2); + + assert.strictEqual(listItem.get(), 3); + assert.strictEqual(listItem.rightmost().get(), 4); + }); + }); + + describe('History', function() { + + it('should be possible to record updates.', function() { + var baobab = new Baobab({item: 1}, {asynchronous: false}), + cursor = baobab.select('item'); + + assert(!cursor.recording); + assert(!cursor.hasHistory()); + assert.deepEqual(cursor.getHistory(), []); + + cursor.startRecording(); + + assert(cursor.recording); + + [1, 2, 3, 4, 5, 6].forEach(function() { + cursor.apply(function(e) { return e + 1; }); + }); + + assert(cursor.hasHistory()); + assert.strictEqual(cursor.get(), 7); + assert.deepEqual(cursor.getHistory(), [2, 3, 4, 5, 6].reverse()); + + cursor.stopRecording(); + cursor.clearHistory(); + + assert(!cursor.recording); + assert(!cursor.hasHistory()); + assert.deepEqual(cursor.getHistory(), []); + }); + + it('should throw an error if trying to undo a recordless cursor.', function() { + var baobab = new Baobab({item: 1}, {asynchronous: false}), + cursor = baobab.select('item'); + + assert.throws(function() { + cursor.undo(); + }, /recording/); + }); + + it('should be possible to go back in time.', function() { + var baobab = new Baobab({item: 1}, {asynchronous: false}), + cursor = baobab.select('item'); + + cursor.startRecording(); + + [1, 2, 3, 4, 5, 6].forEach(function() { + cursor.apply(function(e) { return e + 1; }); + }); + + assert.strictEqual(cursor.get(), 7); + + cursor.undo(); + assert.strictEqual(cursor.get(), 6); + assert.deepEqual(cursor.getHistory(), [2, 3, 4, 5].reverse()); + + cursor.undo().undo(); + + assert.strictEqual(cursor.get(), 4); + assert.deepEqual(cursor.getHistory(), [2, 3].reverse()); + + cursor.set(4).set(5); + + cursor.undo(3); + + assert.strictEqual(cursor.get(), 3); + assert.deepEqual(cursor.getHistory(), [2]); + + assert.throws(function() { + cursor.undo(5); + }, /relevant/); + + assert.throws(function() { + cursor.undo(-5); + }, /positive/); }); }); @@ -446,22 +623,10 @@ describe('Cursor API', function() { }, 0); }); - it('should be possible to push several values through polymorphism.', function(done) { - var baobab = new Baobab({colors: ['blue']}), - colorCursor = baobab.select('colors'); - - colorCursor.push('yellow', 'green'); - - setTimeout(function() { - assert.deepEqual(colorCursor.get(), ['blue', 'yellow', 'green']); - done(); - }, 0); - }); - it('an upper set should correctly resolve.', function(done) { var baobab = new Baobab({hello: {color: 'blue'}}); - baobab.select('hello', 'color').edit('yellow'); + baobab.select('hello', 'color').set('yellow'); baobab.set('hello', 'purple'); baobab.on('update', function() { diff --git a/test/suites/helpers.js b/test/suites/helpers.js index 6c97194..58e898f 100644 --- a/test/suites/helpers.js +++ b/test/suites/helpers.js @@ -6,11 +6,63 @@ var assert = require('assert'), state = require('../state.js'), Baobab = require('../../src/baobab.js'), helpers = require('../../src/helpers.js'), - update = require('../../src/update.js'), - clone = require('lodash.clonedeep'); + update = require('../../src/update.js'); describe('Helpers', function() { + describe('Splice', function() { + var splice = helpers.splice; + + it('should work in a non-mutative fashion.', function() { + var array = ['yellow', 'blue', 'purple']; + + assert.deepEqual( + splice(array, 0, 0), + array + ); + + assert.deepEqual( + splice(array, 0, 1), + ['blue', 'purple'] + ); + + assert.deepEqual( + splice(array, 1, 1), + ['yellow', 'purple'] + ); + + assert.deepEqual( + splice(array, 2, 1), + ['yellow', 'blue'] + ); + + assert.deepEqual( + splice(array, 2, 0), + array + ); + + assert.deepEqual( + splice(array, 1, 2), + ['yellow'] + ); + + assert.deepEqual( + splice(array, 2, 1, 'orange', 'gold'), + ['yellow', 'blue', 'orange', 'gold'] + ); + + assert.deepEqual( + splice(array, 5, 3), + array + ); + + assert.deepEqual( + splice(array, 5, 3, 'orange', 'gold'), + ['yellow', 'blue', 'purple', 'orange', 'gold'] + ); + }); + }); + describe('Composition', function() { it('should be able to compose two simple functions.', function() { @@ -22,8 +74,20 @@ describe('Helpers', function() { }); }); + describe('Decoration', function() { + + it('should be possible to produce a before decoration.', function() { + var count = 0, + inc = function(i) { count++; }, + decorated = helpers.before(inc, inc); + + decorated(); + assert.strictEqual(count, 2); + }); + }); + describe('Nested get', function() { - it('should be possible to retrieve nested items through the helper.', function() { + it('should be possible to retrieve nested items.', function() { assert.deepEqual(helpers.getIn(state, ['one', 'subtwo', 'colors']), state.one.subtwo.colors); assert.strictEqual(helpers.getIn(state, ['primitive']), 3); assert.deepEqual(helpers.getIn(state), state); @@ -52,7 +116,7 @@ describe('Helpers', function() { describe('Solve path', function() { - it('should be able to solve a complex path', function() { + it('should be able to solve a complex path.', function() { var o = { things: [ { @@ -109,8 +173,7 @@ describe('Helpers', function() { it('should be possible to set nested values.', function() { var o1 = {hello: {world: 'one'}}, - o2 = clone(o1); - update(o2, {hello: {world: {$set: 'two'}}}); + o2 = update(o1, {hello: {world: {$set: 'two'}}}).data; assert.deepEqual(o1, {hello: {world: 'one'}}); assert.deepEqual(o2, {hello: {world: 'two'}}); @@ -118,8 +181,7 @@ describe('Helpers', function() { it('should be possible to push to nested values.', function() { var o1 = {colors: ['orange']}, - o2 = clone(o1); - update(o2, {colors: {$push: 'blue'}}); + o2 = update(o1, {colors: {$push: 'blue'}}).data; assert.deepEqual(o1, {colors: ['orange']}); assert.deepEqual(o2, {colors: ['orange', 'blue']}); @@ -127,8 +189,7 @@ describe('Helpers', function() { it('should be possible to unshift to nested values.', function() { var o1 = {colors: ['orange']}, - o2 = clone(o1); - update(o2, {colors: {$unshift: 'blue'}}); + o2 = update(o1, {colors: {$unshift: 'blue'}}).data; assert.deepEqual(o1, {colors: ['orange']}); assert.deepEqual(o2, {colors: ['blue', 'orange']}); @@ -136,15 +197,13 @@ describe('Helpers', function() { it('should be possible to append to nested values.', function() { var o1 = {colors: ['orange']}, - o2 = clone(o1); - update(o2, {colors: {$push: ['blue', 'purple']}}); + o2 = update(o1, {colors: {$push: ['blue', 'purple']}}).data; assert.deepEqual(o1, {colors: ['orange']}); assert.deepEqual(o2, {colors: ['orange', 'blue', 'purple']}); var o3 = {colors: ['orange']}, - o4 = clone(o1); - update(o4, {colors: {$push: 'blue'}}); + o4 = update(o3, {colors: {$push: 'blue'}}).data; assert.deepEqual(o3, {colors: ['orange']}); assert.deepEqual(o4, {colors: ['orange', 'blue']}); @@ -152,15 +211,13 @@ describe('Helpers', function() { it('should be possible to prepend to nested values.', function() { var o1 = {colors: ['orange']}, - o2 = clone(o1); - update(o2, {colors: {$unshift: ['blue', 'purple']}}); + o2 = update(o1, {colors: {$unshift: ['blue', 'purple']}}).data; assert.deepEqual(o1, {colors: ['orange']}); assert.deepEqual(o2, {colors: ['blue', 'purple', 'orange']}); var o3 = {colors: ['orange']}, - o4 = clone(o1); - update(o4, {colors: {$unshift: 'blue'}}); + o4 = update(o3, {colors: {$unshift: 'blue'}}).data; assert.deepEqual(o3, {colors: ['orange']}); assert.deepEqual(o4, {colors: ['blue', 'orange']}); @@ -168,39 +225,32 @@ describe('Helpers', function() { it('should be possible to apply a function to nested values.', function() { var o1 = {number: 10}, - o2 = clone(o1); - update(o2, {number: {$apply: function(n) { return n * 2; }}}); + o2 = update(o1, {number: {$apply: function(n) { return n * 2; }}}).data; assert.deepEqual(o1, {number: 10}); assert.deepEqual(o2, {number: 20}); }); it('should be possible to shallowly merge objects.', function() { - var o = {hey: {one: 1, two: 2}}; - update(o, {hey: {$merge: {three: 3, two: 4}}}); + var o1 = {hey: {one: 1, two: 2}}, + o2 = update(o1, {hey: {$merge: {three: 3, two: 4}}}).data; - assert.deepEqual(o, {hey: {one: 1, two: 4, three: 3}}); + assert.deepEqual(o2, {hey: {one: 1, two: 4, three: 3}}); }); it('should be possible to unset values.', function() { var o1 = {one: 1, two: 2}, - o2 = clone(o1); - update(o2, {one: {$unset: true}}); + o2 = update(o1, {one: {$unset: true}}).data; assert.deepEqual(o1, {one: 1, two: 2}); assert.deepEqual(o2, {two: 2}); }); - it('should be possible to unset values in an array', function() { + it('should be possible to splice an array.', function() { var o1 = {list: [1, 2, 3]}, - o2 = clone(o1); - update(o2, {list: {1: {$unset: true}}}); - - assert.deepEqual(o1, {list: [1, 2, 3]}); - assert.deepEqual(o2, {list: [1, 3]}); + o2 = update(o1, {list: {$splice: [[0, 1], [1, 1, 4]]}}).data; - assert.strictEqual(o1.list.length, 3); - assert.strictEqual(o2.list.length, 2); + assert.deepEqual(o2.list, [2, 4]); }); }); }); diff --git a/test/suites/mixins.js b/test/suites/mixins.js deleted file mode 100644 index 16075dd..0000000 --- a/test/suites/mixins.js +++ /dev/null @@ -1,396 +0,0 @@ -/** - * Baobab Mixins Unit Tests - * ========================= - */ -var assert = require('assert'), - React = require('react/addons'), - Baobab = require('../../src/baobab.js'), - jsdom = require('jsdom').jsdom; - -var testMixin = { - getInitialState: function() { - return {greeting: 'Yeah'}; - } -}; - -describe('React Mixins', function() { - - before(function() { - - // Setting jsdom - var dom = jsdom(''); - global.document = dom; - global.window = dom.parentWindow; - - require('react/lib/ExecutionEnvironment').canUseDOM = true; - }); - - after(function() { - delete global.document; - delete global.window; - }); - - describe('Cursor Mixin', function() { - - it('the mixin should work as stated.', function(done) { - var baobab = new Baobab({hello:'world'}), - cursor = baobab.select('hello'), - i = 0; - - var Component = React.createClass({ - mixins: [cursor.mixin], - render: function() { - assert.strictEqual(this.state.cursor, i ? 'john' : 'world'); - i++; - return React.createElement('div', {id: 'cursor'}, this.cursor.get()); - } - }); - - React.render(React.createElement(Component, null), document.body, function() { - assert.strictEqual(document.querySelector('#cursor').textContent, 'world'); - - baobab.set('hello', 'john'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#cursor').textContent, 'john'); - done(); - }, 0); - }); - }); - - it('should allow mixins in options to access the cursors', function () { - var baobab = new Baobab({ - foo: { - bar: [] - } - }, { - mixins: [{ - componentWillMount: function () { - assert.strictEqual(this.cursor.get(), baobab.select('foo', 'bar').get()); - } - }] - }); - - var Component = React.createClass({ - mixins: [baobab.select('foo', 'bar').mixin], - render: function() { - return React.createElement('div', {}, null); - } - }); - - React.render(React.createElement(Component, null), document.body); - }); - }); - - describe('Tree mixin', function() { - - it('should not break if no cursor is passed to the mixin.', function(done) { - var baobab = new Baobab({hello:'world'}); - - var Component = React.createClass({ - mixins: [baobab.mixin], - render: function() { - return React.createElement('div', {id: 'nocursor'}, 'world'); - } - }); - - React.render(React.createElement(Component, null), document.body, function() { - assert.strictEqual(document.querySelector('#nocursor').textContent, 'world'); - done(); - }); - }); - - it('should be possible to pass a single path.', function(done) { - var baobab = new Baobab({hello:'world'}); - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursor: ['hello'], - render: function() { - return React.createElement('div', {id: 'treepath'}, this.cursor.get()); - } - }); - - React.render(React.createElement(Component, null), document.body, function() { - assert.strictEqual(document.querySelector('#treepath').textContent, 'world'); - - baobab.set('hello', 'john'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepath').textContent, 'john'); - done(); - }, 0); - }); - }); - - it('should be possible to pass a single cursor.', function(done) { - var baobab = new Baobab({hello:'world'}), - i = 0; - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursor: baobab.select('hello'), - render: function() { - assert.strictEqual(this.state.cursor, i ? 'john' : 'world'); - i++; - return React.createElement('div', {id: 'treecursor'}, this.cursor.get()); - } - }); - - React.render(React.createElement(Component, null), document.body, function() { - assert.strictEqual(document.querySelector('#treecursor').textContent, 'world'); - - baobab.set('hello', 'john'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treecursor').textContent, 'john'); - done(); - }, 0); - }); - }); - - it('should be possible to pass a function returning a path.', function(done) { - var baobab = new Baobab({hello:'world'}); - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursor: function() { - return [this.props.pathKey]; - }, - render: function() { - return React.createElement('div', {id: 'treepath'}, this.cursor.get()); - } - }); - - React.render(React.createElement(Component, {pathKey: 'hello'}), document.body, function() { - assert.strictEqual(document.querySelector('#treepath').textContent, 'world'); - - baobab.set('hello', 'john'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepath').textContent, 'john'); - done(); - }, 0); - }); - }); - - it('should be possible to pass a function returning a single cursor.', function(done) { - var baobab = new Baobab({hello:'world'}); - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursor: function() { - return baobab.select(this.props.pathKey); - }, - render: function() { - return React.createElement('div', {id: 'treepath'}, this.cursor.get()); - } - }); - - React.render(React.createElement(Component, {pathKey: 'hello'}), document.body, function() { - assert.strictEqual(document.querySelector('#treepath').textContent, 'world'); - - baobab.set('hello', 'john'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepath').textContent, 'john'); - done(); - }, 0); - }); - }); - - it('should be possible to pass an array of paths.', function(done) { - var baobab = new Baobab({name:'John', surname: 'Talbot'}), - i = 0; - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursors: [['name'], ['surname']], - render: function() { - assert.strictEqual(this.state.cursors[0], i ? 'Jack' : 'John'); - assert.strictEqual(this.state.cursors[1], 'Talbot'); - i++; - return React.createElement('div', {id: 'treepathlist'}, this.cursors[0].get() + ' ' + this.cursors[1].get()); - } - }); - - React.render(React.createElement(Component, null), document.body, function() { - assert.strictEqual(document.querySelector('#treepathlist').textContent, 'John Talbot'); - - baobab.set('name', 'Jack'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepathlist').textContent, 'Jack Talbot'); - done(); - }, 0); - }); - }); - - it('should be possible to pass an array of cursors.', function(done) { - var baobab = new Baobab({name:'John', surname: 'Talbot'}); - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursors: [['name'], baobab.select('surname')], - render: function() { - return React.createElement('div', {id: 'treepathcursors'}, this.cursors[0].get() + ' ' + this.cursors[1].get()); - } - }); - - React.render(React.createElement(Component, null), document.body, function() { - assert.strictEqual(document.querySelector('#treepathcursors').textContent, 'John Talbot'); - - baobab.set('name', 'Jack'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepathcursors').textContent, 'Jack Talbot'); - done(); - }, 0); - }); - }); - - it('should be possible to pass an object of paths.', function(done) { - var baobab = new Baobab({name:'John', surname: 'Talbot'}), - i = 0; - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursors: { - name: ['name'], - surname: ['surname'] - }, - render: function() { - assert.strictEqual(this.state.cursors.name, i ? 'Jack' : 'John'); - assert.strictEqual(this.state.cursors.surname, 'Talbot'); - i++; - return React.createElement('div', {id: 'treepathobject'}, this.cursors.name.get() + ' ' + this.cursors.surname.get()); - } - }); - - React.render(React.createElement(Component, null), document.body, function() { - assert.strictEqual(document.querySelector('#treepathobject').textContent, 'John Talbot'); - - baobab.set('name', 'Jack'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepathobject').textContent, 'Jack Talbot'); - done(); - }, 0); - }); - }); - - it('should be possible to pass an object of cursors.', function(done) { - var baobab = new Baobab({name:'John', surname: 'Talbot'}); - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursors: { - name: ['name'], - surname: baobab.select('surname') - }, - render: function() { - return React.createElement('div', {id: 'treepathoc'}, this.cursors.name.get() + ' ' + this.cursors.surname.get()); - } - }); - - React.render(React.createElement(Component, null), document.body, function() { - assert.strictEqual(document.querySelector('#treepathoc').textContent, 'John Talbot'); - - baobab.set('name', 'Jack'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepathoc').textContent, 'Jack Talbot'); - done(); - }, 0); - }); - }); - - it('should be possible to pass a function returning an array of cursors.', function(done) { - var baobab = new Baobab({name:'John', surname: 'Talbot'}); - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursors: function() { - return [[this.props.pathKey1], baobab.select(this.props.pathKey2)]; - }, - render: function() { - return React.createElement('div', {id: 'treepathcursors'}, this.cursors[0].get() + ' ' + this.cursors[1].get()); - } - }); - - React.render(React.createElement(Component, {pathKey1: 'name', pathKey2: 'surname'}), document.body, function() { - assert.strictEqual(document.querySelector('#treepathcursors').textContent, 'John Talbot'); - - baobab.set('name', 'Jack'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepathcursors').textContent, 'Jack Talbot'); - done(); - }, 0); - }); - }); - - it('should be possible to pass function returning an object of cursors.', function(done) { - var baobab = new Baobab({name:'John', surname: 'Talbot'}); - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursors: function() { - return { - name: [this.props.pathKey1], - surname: baobab.select(this.props.pathKey2) - }; - }, - render: function() { - return React.createElement('div', {id: 'treepathoc'}, this.cursors.name.get() + ' ' + this.cursors.surname.get()); - } - }); - - React.render(React.createElement(Component, {pathKey1: 'name', pathKey2: 'surname'}), document.body, function() { - assert.strictEqual(document.querySelector('#treepathoc').textContent, 'John Talbot'); - - baobab.set('name', 'Jack'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepathoc').textContent, 'Jack Talbot'); - done(); - }, 0); - }); - }); - - it('should be possible to pass custom mixins.', function(done) { - var baobab = new Baobab({name:'John', surname: 'Talbot'}, {mixins: [testMixin]}); - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursor: ['name'], - render: function() { - return React.createElement('div', {id: 'treepathmixin'}, this.state.greeting + ' ' + this.cursor.get()); - } - }); - - React.render(React.createElement(Component, null), document.body, function() { - assert.strictEqual(document.querySelector('#treepathmixin').textContent, 'Yeah John'); - - baobab.set('name', 'Jack'); - setTimeout(function() { - assert.strictEqual(document.querySelector('#treepathmixin').textContent, 'Yeah Jack'); - done(); - }, 0); - }); - }); - - it('should allow mixins in options to access the tree', function () { - var baobab = new Baobab({ - items: [] - }, { - mixins: [{ - componentWillMount: function () { - assert.strictEqual(this.cursor.get(), baobab.select('items').get()); - } - }] - }); - - var Component = React.createClass({ - mixins: [baobab.mixin], - cursor: ['items'], - render: function() { - return React.createElement('div', {}, null); - } - }); - - React.render(React.createElement(Component, null), document.body); - - }); - - }); -});