├── .gitignore ├── .travis.yml ├── README.md ├── api.md ├── dist ├── immstruct.js └── immstruct.min.js ├── index.js ├── makeBundle.js ├── package.json ├── src ├── structure.js └── utils.js ├── structure.js └── tests ├── immstruct_test.js └── structure_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Immstruct [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][depstat-image]][depstat-url] [![Gitter][gitter-image]][gitter-url] 2 | ====== 3 | 4 | A wrapper for [Immutable.js](https://github.com/facebook/immutable-js/tree/master/contrib/cursor) to easily create cursors that notify when they 5 | are updated. Handy for use with immutable pure components for views, 6 | like with [Omniscient](https://github.com/omniscientjs/omniscient) or [React.js](https://github.com/facebook/react). 7 | 8 | See the [API References](./api.md) for more documentation and usage. 9 | 10 | ## Usage 11 | 12 | ### `someFile.js` 13 | ```js 14 | 15 | // Require a default instance of immstruct (singleton instance) 16 | var immstruct = require('immstruct'); 17 | 18 | // Create structure under the later retrievable ID `myKey` 19 | var structure = immstruct('myKey', { a: { b: { c: 1 } } }); 20 | 21 | // Use event `swap` or `next-animation-frame` 22 | structure.on('swap', function (newStructure, oldStructure, keyPath) { 23 | console.log('Subpart of structure swapped.'); 24 | console.log('New structure:', newStructure.toJSON()); 25 | 26 | // e.g. with usage with React 27 | // React.render(App({ cursor: structure.cursor() }), document.body); 28 | }); 29 | 30 | var cursor = structure.cursor(['a', 'b', 'c']); 31 | 32 | // Update the value at the cursor. As cursors are immutable, 33 | // this returns a new cursor that points to the new data 34 | var newCursor = cursor.update(function (x) { 35 | return x + 1; 36 | }); 37 | 38 | // We unwrap the cursor, by getting the data it is pointing at using deref 39 | // and see that the value of the old `cursor` to is still `1` 40 | console.log(cursor.deref()); //=> 1 41 | 42 | // `newCursor` points to the new data 43 | console.log(newCursor.deref()); //=> 2 44 | ``` 45 | 46 | **Note:** *The cursors you see here are cursors from Facebooks Immutable.js library. Read the 47 | complete API in [their repo](https://github.com/facebook/immutable-js/blob/master/contrib/cursor/index.d.ts).* 48 | 49 | ### `anotherFile.js` 50 | ```js 51 | // Require a default instance of immstruct (singleton instance) 52 | var immstruct = require('immstruct'); 53 | 54 | // Get the structure we previously defined under the ID `myKey` 55 | var structure = immstruct('myKey'); 56 | 57 | var cursor = structure.cursor(['a', 'b', 'c']); 58 | 59 | var updatedCursor = cursor.update(function (x) { // triggers `swap` in somefile.js 60 | return x + 1; 61 | }); 62 | 63 | // Unwrap the value 64 | console.log(updatedCursor.deref()); //=> 3 65 | ``` 66 | 67 | ## References 68 | 69 | While Immutable.js cursors are immutable, Immstruct lets you create references 70 | to a piece of data from where cursors will always be fresh. The cursors you create 71 | are still immutable, but you have the ability to retrieve the newest and latest 72 | cursor pointing to a specific part of your immutable structure. 73 | 74 | ```js 75 | 76 | var structure = immstruct({ 'foo': 'bar' }); 77 | var ref = structure.reference('foo'); 78 | 79 | console.log(ref.cursor().deref()) //=> 'bar' 80 | 81 | var oldCursor = structure.cursor('foo'); 82 | console.log(oldCursor.deref()) //=> 'bar' 83 | 84 | var newCursor = structure.cursor('foo').update(function () { return 'updated'; }); 85 | console.log(newCursor.deref()) //=> 'updated' 86 | 87 | assert(oldCursor !== newCursor); 88 | 89 | // You don't need to manage and track fresh/stale cursors. 90 | // A reference cursor will do it for you. 91 | console.log(ref.cursor().deref()) //=> 'updated' 92 | ``` 93 | 94 | Updating a cursor created from a reference will also update the underlying structure. 95 | 96 | This offers benefits similar to that of [Om](https://github.com/omcljs/om/wiki/Advanced-Tutorial#reference-cursors)'s `reference cursors`, where 97 | [React.js](http://facebook.github.io/react/) or [Omniscient](https://github.com/omniscientjs/omniscient/) components can observe pieces of application 98 | state without it being passed as cursors in props from their parent components. 99 | 100 | References also allow for listeners that fire when their path or the path of sub-cursors change: 101 | 102 | ```js 103 | var structure = immstruct({ 104 | someBox: { message: 'Hello World!' } 105 | }); 106 | var ref = structure.reference(['someBox']); 107 | 108 | var unobserve = ref.observe(function () { 109 | // Called when data the path 'someBox' is changed. 110 | // Also called when the data at ['someBox', 'message'] is changed. 111 | }); 112 | 113 | // Update the data using the ref 114 | ref.cursor().update(function () { return 'updated'; }); 115 | 116 | // Update the data using the initial structure 117 | structure.cursor(['someBox', 'message']).update(function () { return 'updated again'; }); 118 | 119 | // Remove the listener 120 | unobserve(); 121 | ``` 122 | 123 | ### Notes 124 | 125 | Parents' change listeners are also called when sub-cursors are changed. 126 | 127 | Cursors created from references are still immutable. If you keep a cursor from 128 | a `var cursor = reference.cursor()` around, the `cursor` will still point to the data 129 | at time of cursor creation. Updating it may rewrite newer information. 130 | 131 | ## Usage Undo/Redo 132 | 133 | ```js 134 | // optionalKey and/or optionalLimit can be omitted from the call 135 | var optionalLimit = 10; // only keep last 10 of history, default Infinity 136 | var structure = immstruct.withHistory('optionalKey', optionalLimit, { 'foo': 'bar' }); 137 | console.log(structure.cursor('foo').deref()); //=> 'bar' 138 | 139 | structure.cursor('foo').update(function () { return 'hello'; }); 140 | console.log(structure.cursor('foo').deref()); //=> 'hello' 141 | 142 | structure.undo(); 143 | console.log(structure.cursor('foo').deref()); //=> 'bar' 144 | 145 | structure.redo(); 146 | console.log(structure.cursor('foo').deref()); //=> 'hello' 147 | 148 | ``` 149 | 150 | ## Examples 151 | 152 | Creates or retrieves [structures](#structure--eventemitter). 153 | 154 | See examples: 155 | 156 | ```js 157 | var structure = immstruct('someKey', { some: 'jsObject' }) 158 | // Creates new structure with someKey 159 | ``` 160 | 161 | 162 | ```js 163 | var structure = immstruct('someKey') 164 | // Get's the structure named `someKey`. 165 | ``` 166 | 167 | **Note:** if someKey doesn't exist, an empty structure is created 168 | 169 | ```js 170 | var structure = immstruct({ some: 'jsObject' }) 171 | var randomGeneratedKey = structure.key; 172 | // Creates a new structure with random key 173 | // Used if key is not necessary 174 | ``` 175 | 176 | 177 | ```js 178 | var structure = immstruct() 179 | var randomGeneratedKey = structure.key; 180 | // Create new empty structure with random key 181 | ``` 182 | 183 | You can also create your own instance of Immstruct, isolating the 184 | different instances of structures: 185 | 186 | ```js 187 | var localImmstruct = new immstruct.Immstruct() 188 | var structure = localImmstruct.get('someKey', { my: 'object' }); 189 | ``` 190 | 191 | ## API Reference 192 | 193 | See [API Reference](./api.md). 194 | 195 | ## Structure Events 196 | 197 | A Structure object is an event emitter and emits the following events: 198 | 199 | * `swap`: Emitted when cursor is updated (new information is set). Is emitted 200 | on all types of changes, additions and deletions. The passed structures are 201 | always the root structure. 202 | One use case for this is to re-render design components. Callback 203 | is passed arguments: `newStructure`, `oldStructure`, `keyPath`. 204 | * `next-animation-frame`: Same as `swap`, but only emitted on animation frame. 205 | Could use with many render updates and better performance. Callback is passed 206 | arguments: `newStructure`, `oldStructure`, `keyPath`. 207 | * `change`: Emitted when data/value is updated and it existed before. Emits 208 | values: `newValue`, `oldValue` and `path`. 209 | * `delete`: Emitted when data/value is removed. Emits value: `removedValue` and `path`. 210 | * `add`: Emitted when new data/value is added. Emits value: `newValue` and `path`. 211 | * `any`: With the same semantics as `add`, `change` or `delete`, `any` is triggered for 212 | all types of changes. Differs from swap in the arguments that it is passed. 213 | Is passed `newValue` (or undefined), `oldValue` (or undefined) and full `keyPath`. 214 | New and old value are the changed value, not relative/scoped to the reference path as 215 | with `swap`. 216 | 217 | **NOTE:** If you update cursors via `Cursor.update` or `Cursor.set`, and if the 218 | underlying Immutable collection is not inherently changed, `swap` and `changed` 219 | events will not be emitted, neither will the history (if any) be applied. 220 | 221 | [See tests for event examples](./tests/structure_test.js) 222 | 223 | [npm-url]: https://npmjs.org/package/immstruct 224 | [npm-image]: http://img.shields.io/npm/v/immstruct.svg?style=flat 225 | 226 | [travis-url]: http://travis-ci.org/omniscientjs/immstruct 227 | [travis-image]: http://img.shields.io/travis/omniscientjs/immstruct.svg?style=flat 228 | 229 | [depstat-url]: https://gemnasium.com/omniscientjs/immstruct 230 | [depstat-image]: http://img.shields.io/gemnasium/omniscientjs/immstruct.svg?style=flat 231 | 232 | [gitter-url]: https://gitter.im/omniscientjs/omniscient?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 233 | [gitter-image]: https://badges.gitter.im/Join%20Chat.svg 234 | 235 | ## License 236 | 237 | [MIT License](http://en.wikipedia.org/wiki/MIT_License) 238 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | 2 | ### `Immstruct` 3 | 4 | Creates a new instance of Immstruct, having it's own list 5 | of Structure instances. 6 | 7 | ### Examples: 8 | ```js 9 | var ImmstructInstance = require('immstruct').Immstruct; 10 | var immstruct = new ImmstructInstance(); 11 | var structure = immstruct.get({ data: }); 12 | ``` 13 | 14 | 15 | ### Properties 16 | 17 | | property | type | description | 18 | | ----------- | ----------------- | -------------------------------- | 19 | | `instances` | Array. | Array of `Structure` instances. | 20 | 21 | 22 | 23 | **Returns** `Immstruct`, 24 | 25 | 26 | ### `immstruct.get([key], [data])` 27 | 28 | Gets or creates a new instance of {Structure}. Provide optional 29 | key to be able to retrieve it from list of instances. If no key 30 | is provided, a random key will be generated. 31 | 32 | ### Examples: 33 | ```js 34 | var immstruct = require('immstruct'); 35 | var structure = immstruct.get('myStruct', { foo: 'Hello' }); 36 | ``` 37 | 38 | ### Parameters 39 | 40 | | param | type | description | 41 | | -------- | ---------------- | --------------------------------------- | 42 | | `[key]` | string | _optional:_ - defaults to random string | 43 | | `[data]` | Object,Immutable | _optional:_ - defaults to empty data | 44 | 45 | 46 | 47 | **Returns** `Structure`, 48 | 49 | 50 | ### `immstruct.instance([name])` 51 | 52 | Get list of all instances created. 53 | 54 | 55 | ### Parameters 56 | 57 | | param | type | description | 58 | | -------- | ------ | -------------------------------------------------------------------------- | 59 | | `[name]` | string | _optional:_ - Name of the instance to get. If undefined get all instances | 60 | 61 | 62 | 63 | **Returns** `Structure,Object.`, 64 | 65 | 66 | ### `immstruct.clear` 67 | 68 | Clear the entire list of `Structure` instances from the Immstruct 69 | instance. You would do this to start from scratch, freeing up memory. 70 | 71 | ### Examples: 72 | ```js 73 | var immstruct = require('immstruct'); 74 | immstruct.clear(); 75 | ``` 76 | 77 | 78 | ### `immstruct.remove(key)` 79 | 80 | Remove one `Structure` instance from the Immstruct instances list. 81 | Provided by key 82 | 83 | ### Examples: 84 | ```js 85 | var immstruct = require('immstruct'); 86 | immstruct('myKey', { foo: 'hello' }); 87 | immstruct.remove('myKey'); 88 | ``` 89 | 90 | ### Parameters 91 | 92 | | param | type | description | 93 | | ----- | ------ | ----------- | 94 | | `key` | string | | 95 | 96 | 97 | 98 | **Returns** `boolean`, 99 | 100 | 101 | ### `immstruct.withHistory([key], [limit], [data])` 102 | 103 | Gets or creates a new instance of `Structure` with history (undo/redo) 104 | activated per default. Same usage and signature as regular `Immstruct.get`. 105 | 106 | Provide optional key to be able to retrieve it from list of instances. 107 | If no key is provided, a random key will be generated. 108 | 109 | Provide optional limit to cap the last number of history references 110 | that will be kept. Once limit is reached, a new history record 111 | shifts off the oldest record. The default if omitted is Infinity. 112 | Setting to 0 is the as not having history enabled in the first place. 113 | 114 | ### Examples: 115 | ```js 116 | var immstruct = require('immstruct'); 117 | var structure = immstruct.withHistory('myStruct', 10, { foo: 'Hello' }); 118 | var structure = immstruct.withHistory(10, { foo: 'Hello' }); 119 | var structure = immstruct.withHistory('myStruct', { foo: 'Hello' }); 120 | var structure = immstruct.withHistory({ foo: 'Hello' }); 121 | ``` 122 | 123 | 124 | ### Parameters 125 | 126 | | param | type | description | 127 | | --------- | ---------------- | --------------------------------------- | 128 | | `[key]` | string | _optional:_ - defaults to random string | 129 | | `[limit]` | number | _optional:_ - defaults to Infinity | 130 | | `[data]` | Object,Immutable | _optional:_ - defaults to empty data | 131 | 132 | 133 | 134 | **Returns** `Structure`, 135 | 136 | 137 | ### `immstruct([key], [data])` 138 | 139 | This is a default instance of `Immstruct` as well as a shortcut for 140 | creating `Structure` instances (See `Immstruct.get` and `Immstruct`). 141 | This is what is returned from `require('immstruct')`. 142 | 143 | From `Immstruct.get`: 144 | Gets or creates a new instance of {Structure} in the default Immstruct 145 | instance. A link to `immstruct.get()`. Provide optional 146 | key to be able to retrieve it from list of instances. If no key 147 | is provided, a random key will be generated. 148 | 149 | ### Examples: 150 | ```js 151 | var immstruct = require('immstruct'); 152 | var structure = immstruct('myStruct', { foo: 'Hello' }); 153 | var structure2 = immstruct.withHistory({ bar: 'Bye' }); 154 | immstruct.remove('myStruct'); 155 | // ... 156 | ``` 157 | 158 | 159 | ### Parameters 160 | 161 | | param | type | description | 162 | | -------- | ---------------- | --------------------------------------- | 163 | | `[key]` | string | _optional:_ - defaults to random string | 164 | | `[data]` | Object,Immutable | _optional:_ - defaults to empty data | 165 | 166 | 167 | 168 | **Returns** `Structure,Function`, 169 | 170 | 171 | ### `Structure([options])` 172 | 173 | Creates a new `Structure` instance. Also accessible through 174 | `Immstruct.Structre`. 175 | 176 | A structure is also an EventEmitter object, so it has methods as 177 | `.on`, `.off`, and all other EventEmitter methods. 178 | 179 | 180 | For the `swap` event, the root structure (see `structure.current`) is passed 181 | as arguments, but for type specific events (`add`, `change` and `delete`), the 182 | actual changed value is passed. 183 | 184 | For instance: 185 | ```js 186 | var structure = new Structure({ 'data': { 'foo': { 'bar': 'hello' } } }); 187 | 188 | structure.on('swap', function (newData, oldData, keyPath) { 189 | keyPath.should.eql(['foo', 'bar']); 190 | newData.toJS().should.eql({ 'foo': { 'bar': 'bye' } }); 191 | oldData.toJS().should.eql({ 'foo': { 'bar': 'hello' } }); 192 | }); 193 | 194 | structure.cursor(['foo', 'bar']).update(function () { 195 | return 'bye'; 196 | }); 197 | ``` 198 | 199 | But for `change` 200 | ```js 201 | var structure = new Structure({ 'data': { 'foo': { 'bar': 'hello' } } }); 202 | 203 | structure.on('change', function (newData, oldData, keyPath) { 204 | keyPath.should.eql(['foo', 'bar']); 205 | newData.should.eql('bye'); 206 | oldData.should.eql('hello'); 207 | }); 208 | 209 | structure.cursor(['foo', 'bar']).update(function () { 210 | return 'bye'; 211 | }); 212 | ``` 213 | 214 | **All `keyPath`s passed to listeners are the full path to where the actual 215 | change happened** 216 | 217 | ### Examples: 218 | ```js 219 | var Structure = require('immstruct/structure'); 220 | var s = new Structure({ data: { foo: 'bar' }}); 221 | 222 | // Or: 223 | // var Structure = require('immstruct').Structure; 224 | ``` 225 | 226 | ### Events 227 | 228 | * `swap`: Emitted when cursor is updated (new information is set). Is emitted 229 | on all types of changes, additions and deletions. The passed structures are 230 | always the root structure. 231 | One use case for this is to re-render design components. Callback 232 | is passed arguments: `newStructure`, `oldStructure`, `keyPath`. 233 | * `next-animation-frame`: Same as `swap`, but only emitted on animation frame. 234 | Could use with many render updates and better performance. Callback is passed 235 | arguments: `newStructure`, `oldStructure`, `keyPath`. 236 | * `change`: Emitted when data/value is updated and it existed before. Emits 237 | values: `newValue`, `oldValue` and `path`. 238 | * `delete`: Emitted when data/value is removed. Emits value: `removedValue` and `path`. 239 | * `add`: Emitted when new data/value is added. Emits value: `newValue` and `path`. 240 | * `any`: With the same semantics as `add`, `change` or `delete`, `any` is triggered for 241 | all types of changes. Differs from swap in the arguments that it is passed. 242 | Is passed `newValue` (or undefined), `oldValue` (or undefined) and full `keyPath`. 243 | New and old value are the changed value, not relative/scoped to the reference path as 244 | with `swap`. 245 | 246 | ### Options 247 | 248 | ```json 249 | { 250 | key: string, // Defaults to random string 251 | data: Object|Immutable, // defaults to empty Map 252 | history: boolean, // Defaults to false 253 | historyLimit: number, // If history enabled, Defaults to Infinity 254 | } 255 | ``` 256 | 257 | 258 | ### Parameters 259 | 260 | | param | type | description | 261 | | ----------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | 262 | | `[options]` | { key: string, data: Object, history: boolean } | _optional:_ - defaults to random key and empty data (immutable structure). No history 263 | | 264 | 265 | 266 | ### Properties 267 | 268 | | property | type | description | 269 | | --------- | ---------------- | ------------------------------- | 270 | | `history` | Immutable.List | `Immutable.List` with history. | 271 | | `current` | Object,Immutable | Provided data as immutable data | 272 | | `key` | string | Generated or provided key. 273 | | 274 | 275 | 276 | 277 | **Returns** `Structure`, 278 | 279 | 280 | ### `structure.cursor([path])` 281 | 282 | Create a Immutable.js Cursor for a given `path` on the `current` structure (see `Structure.current`). 283 | Changes made through created cursor will cause a `swap` event to happen (see `Events`). 284 | 285 | **This method returns a 286 | [Immutable.js Cursor](https://github.com/facebook/immutable-js/blob/master/contrib/cursor/index.d.ts). 287 | See the Immutable.js docs for more info on how to use cursors.** 288 | 289 | ### Examples: 290 | ```js 291 | var Structure = require('immstruct/structure'); 292 | var s = new Structure({ data: { foo: 'bar', a: { b: 'foo' } }}); 293 | s.cursor().set('foo', 'hello'); 294 | s.cursor('foo').update(function () { return 'Changed'; }); 295 | s.cursor(['a', 'b']).update(function () { return 'bar'; }); 296 | ``` 297 | 298 | See more examples in the [tests](https://github.com/omniscientjs/immstruct/blob/master/tests/structure_test.js) 299 | 300 | 301 | ### Parameters 302 | 303 | | param | type | description | 304 | | -------- | --------------------- | ---------------------------------------------------------------------------------------- | 305 | | `[path]` | string,Array. | _optional:_ - defaults to empty string. Can be array for path. See Immutable.js Cursors | 306 | 307 | 308 | 309 | **Returns** `Cursor`, Gives a Cursor from Immutable.js 310 | 311 | 312 | ### `structure.reference([path|cursor])` 313 | 314 | Creates a reference. A reference can be a pointer to a cursor, allowing 315 | you to create cursors for a specific path any time. This is essentially 316 | a way to have "always updated cursors" or Reference Cursors. See example 317 | for better understanding the concept. 318 | 319 | References also allow you to listen for changes specific for a path. 320 | 321 | ### Examples: 322 | ```js 323 | var structure = immstruct({ 324 | someBox: { message: 'Hello World!' } 325 | }); 326 | var ref = structure.reference(['someBox']); 327 | 328 | var unobserve = ref.observe(function () { 329 | // Called when data the path 'someBox' is changed. 330 | // Also called when the data at ['someBox', 'message'] is changed. 331 | }); 332 | 333 | // Update the data using the ref 334 | ref.cursor().update(function () { return 'updated'; }); 335 | 336 | // Update the data using the initial structure 337 | structure.cursor(['someBox', 'message']).update(function () { return 'updated again'; }); 338 | 339 | // Remove the listener 340 | unobserve(); 341 | ``` 342 | 343 | See more examples in the [readme](https://github.com/omniscientjs/immstruct) 344 | 345 | 346 | ### Parameters 347 | 348 | | param | type | description | 349 | | --------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | 350 | | `[path|cursor]` | string,Array.,Cursor | _optional:_ - defaults to empty string. Can be array for path or use path of cursor. See Immutable.js Cursors 351 | | 352 | 353 | 354 | 355 | **Returns** `Reference`, 356 | 357 | 358 | ### `reference.observe([eventName], callback)` 359 | 360 | Observe for changes on a reference. On references you can observe for changes, 361 | but a reference **is not** an EventEmitter it self. 362 | 363 | The passed `keyPath` for swap events are relative to the reference, but 364 | 365 | 366 | **Note**: As on `swap` for normal immstruct events, the passed arguments for 367 | the event is the root, not guaranteed to be the actual changed value. 368 | The structure is how ever scoped to the path passed in to the reference. 369 | All values passed to the eventlistener for the swap event are relative 370 | to the path used as key path to the reference. 371 | 372 | For instance: 373 | 374 | ```js 375 | var structure = immstruct({ 'foo': { 'bar': 'hello' } }); 376 | var ref = structure.reference('foo'); 377 | ref.observe(function (newData, oldData, keyPath) { 378 | keyPath.should.eql(['bar']); 379 | newData.toJS().should.eql({ 'bar': 'updated' }); 380 | oldData.toJS().should.eql({ 'bar': 'hello' }); 381 | }); 382 | ref.cursor().update(['bar'], function () { return 'updated'; }); 383 | ``` 384 | 385 | For type specific events, how ever, the actual changed value is passed, 386 | not the root data. In these cases, the full keyPath to the change is passed. 387 | 388 | For instance: 389 | 390 | ```js 391 | var structure = immstruct({ 'foo': { 'bar': 'hello' } }); 392 | var ref = structure.reference('foo'); 393 | ref.observe('change', function (newValue, oldValue, keyPath) { 394 | keyPath.should.eql(['foo', 'bar']); 395 | newData.should.eql('updated'); 396 | oldData.should.eql('hello'); 397 | }); 398 | ref.cursor().update(['bar'], function () { return 'updated'; }); 399 | ``` 400 | 401 | 402 | ### Examples: 403 | ```js 404 | var ref = structure.reference(['someBox']); 405 | 406 | var unobserve = ref.observe('delete', function () { 407 | // Called when data the path 'someBox' is removed from the structure. 408 | }); 409 | ``` 410 | 411 | See more examples in the [readme](https://github.com/omniscientjs/immstruct) 412 | 413 | ### Events 414 | * `swap`: Emitted when any cursor is updated (new information is set). 415 | Triggered in any data swap is made on the structure. One use case for 416 | this is to re-render design components. Data passed as arguments 417 | are scoped/relative to the path passed to the reference, this also goes for keyPath. 418 | Callback is passed arguments: `newStructure`, `oldStructure`, `keyPath`. 419 | * `change`: Emitted when data/value is updated and it existed before. 420 | Emits values: `newValue`, `oldValue` and `path`. 421 | * `delete`: Emitted when data/value is removed. Emits value: `removedValue` and `path`. 422 | * `add`: Emitted when new data/value is added. Emits value: `newValue` and `path`. 423 | * `any`: With the same semantics as `add`, `change` or `delete`, `any` is triggered for 424 | all types of changes. Differs from swap in the arguments that it is passed. 425 | Is passed `newValue` (or undefined), `oldValue` (or undefined) and full `keyPath`. 426 | New and old value are the changed value, not relative/scoped to the reference path as 427 | with `swap`. 428 | 429 | 430 | ### Parameters 431 | 432 | | param | type | description | 433 | | ------------- | -------- | ------------------------------------------- | 434 | | `[eventName]` | string | _optional:_ - Type of change | 435 | | `callback` | Function | - Callback when referenced data is swapped | 436 | 437 | 438 | 439 | **Returns** `Function`, Function for removing observer (unobserve) 440 | 441 | 442 | ### `reference.cursor([subpath])` 443 | 444 | Create a new, updated, cursor from the base path provded to the 445 | reference. This returns a Immutable.js Cursor as the regular 446 | cursor method. You can also provide a sub-path to create a reference 447 | in a deeper level. 448 | 449 | ### Examples: 450 | ```js 451 | var ref = structure.reference(['someBox']); 452 | var cursor = ref.cursor('someSubPath'); 453 | var cursor2 = ref.cursor(); 454 | ``` 455 | 456 | See more examples in the [readme](https://github.com/omniscientjs/immstruct) 457 | 458 | 459 | ### Parameters 460 | 461 | | param | type | description | 462 | | ----------- | ------ | -------------------------------------------- | 463 | | `[subpath]` | string | _optional:_ - Subpath to a deeper structure | 464 | 465 | 466 | 467 | **Returns** `Cursor`, Immutable.js cursor 468 | 469 | 470 | ### `reference.reference([path])` 471 | 472 | Creates a reference on a lower level path. See creating normal references. 473 | 474 | ### Examples: 475 | ```js 476 | var structure = immstruct({ 477 | someBox: { message: 'Hello World!' } 478 | }); 479 | var ref = structure.reference('someBox'); 480 | 481 | var newReference = ref.reference('message'); 482 | ``` 483 | 484 | See more examples in the [readme](https://github.com/omniscientjs/immstruct) 485 | 486 | 487 | ### Parameters 488 | 489 | | param | type | description | 490 | | -------- | --------------------- | ---------------------------------------------------------------------------------------- | 491 | | `[path]` | string,Array. | _optional:_ - defaults to empty string. Can be array for path. See Immutable.js Cursors | 492 | 493 | 494 | 495 | **Returns** `Reference`, 496 | 497 | 498 | ### `reference.unobserveAll` 499 | 500 | Remove all observers from reference. 501 | 502 | 503 | 504 | **Returns** `Void`, 505 | 506 | 507 | ### `reference.destroy` 508 | 509 | Destroy reference. Unobserve all observers, set all endpoints of reference to dead. 510 | For cleaning up memory. 511 | 512 | 513 | 514 | **Returns** `Void`, 515 | 516 | 517 | ### `structure.forceHasSwapped(newData, oldData, keyPath)` 518 | 519 | Force emitting swap event. Pass on new, old and keypath passed to swap. 520 | If newData is `null` current will be used. 521 | 522 | 523 | ### Parameters 524 | 525 | | param | type | description | 526 | | --------- | ------ | --------------------------------------------------------- | 527 | | `newData` | Object | - Immutable object for the new data to emit | 528 | | `oldData` | Object | - Immutable object for the old data to emit | 529 | | `keyPath` | string | - Structure path (in tree) to where the changes occured. | 530 | 531 | 532 | 533 | **Returns** `Void`, 534 | 535 | 536 | ### `structure.undo(steps)` 537 | 538 | Undo IFF history is activated and there are steps to undo. Returns new current 539 | immutable structure. 540 | 541 | **Will NOT emit swap when redo. You have to do this yourself**. 542 | 543 | Define number of steps to undo in param. 544 | 545 | 546 | ### Parameters 547 | 548 | | param | type | description | 549 | | ------- | ------ | -------------------------- | 550 | | `steps` | number | - Number of steps to undo | 551 | 552 | 553 | 554 | **Returns** `Object`, New Immutable structure after undo 555 | 556 | 557 | ### `structure.redo(head)` 558 | 559 | Redo IFF history is activated and you can redo. Returns new current immutable structure. 560 | Define number of steps to redo in param. 561 | **Will NOT emit swap when redo. You have to do this yourself**. 562 | 563 | 564 | ### Parameters 565 | 566 | | param | type | description | 567 | | ------ | ------ | ------------------------------------- | 568 | | `head` | number | - Number of steps to head to in redo | 569 | 570 | 571 | 572 | **Returns** `Object`, New Immutable structure after redo 573 | 574 | 575 | ### `structure.undoUntil(structure)` 576 | 577 | Undo IFF history is activated and passed `structure` exists in history. 578 | Returns the same immutable structure as passed as argument. 579 | 580 | **Will NOT emit swap after undo. You have to do this yourself**. 581 | 582 | 583 | ### Parameters 584 | 585 | | param | type | description | 586 | | ----------- | ------ | ------------------------------------ | 587 | | `structure` | Object | - Immutable structure to redo until | 588 | 589 | 590 | 591 | **Returns** `Object`, New Immutable structure after undo 592 | 593 | ## Private members 594 | 595 | 596 | -------------------------------------------------------------------------------- /dist/immstruct.js: -------------------------------------------------------------------------------- 1 | /** 2 | * immstruct v2.0.0 3 | * A part of the Omniscient.js project 4 | ***************************************/ 5 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.immstruct = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 2; 801 | var newRootData = cursor._rootData.updateIn( 802 | cursor._keyPath, 803 | deepChange ? Map() : undefined, 804 | changeFn 805 | ); 806 | var keyPath = cursor._keyPath || []; 807 | var result = cursor._onChange && cursor._onChange.call( 808 | undefined, 809 | newRootData, 810 | cursor._rootData, 811 | deepChange ? newKeyPath(keyPath, changeKeyPath) : keyPath 812 | ); 813 | if (result !== undefined) { 814 | newRootData = result; 815 | } 816 | return makeCursor(newRootData, cursor._keyPath, cursor._onChange); 817 | } 818 | 819 | function newKeyPath(head, tail) { 820 | return head.concat(listToKeyPath(tail)); 821 | } 822 | 823 | function listToKeyPath(list) { 824 | return Array.isArray(list) ? list : Immutable.Iterable(list).toArray(); 825 | } 826 | 827 | function valToKeyPath(val) { 828 | return Array.isArray(val) ? val : 829 | Iterable.isIterable(val) ? val.toArray() : 830 | [val]; 831 | } 832 | 833 | exports.from = cursorFrom; 834 | 835 | },{"immutable":undefined}],4:[function(_dereq_,module,exports){ 836 | (function (global){ 837 | 'use strict'; 838 | 839 | var Immutable = (typeof window !== "undefined" ? window['Immutable'] : typeof global !== "undefined" ? global['Immutable'] : null); 840 | var Cursor = _dereq_('immutable/contrib/cursor/index'); 841 | var EventEmitter = _dereq_('eventemitter3'); 842 | var utils = _dereq_('./utils'); 843 | 844 | var LISTENER_SENTINEL = {}; 845 | 846 | /** 847 | * Creates a new `Structure` instance. Also accessible through 848 | * `Immstruct.Structre`. 849 | * 850 | * A structure is also an EventEmitter object, so it has methods as 851 | * `.on`, `.off`, and all other EventEmitter methods. 852 | * 853 | * 854 | * For the `swap` event, the root structure (see `structure.current`) is passed 855 | * as arguments, but for type specific events (`add`, `change` and `delete`), the 856 | * actual changed value is passed. 857 | * 858 | * For instance: 859 | * ```js 860 | * var structure = new Structure({ 'foo': { 'bar': 'hello' } }); 861 | * 862 | * structure.on('swap', function (newData, oldData, keyPath) { 863 | * keyPath.should.eql(['foo', 'bar']); 864 | * newData.toJS().should.eql({ 'foo': { 'bar': 'bye' } }); 865 | * oldData.toJS().should.eql({ 'foo': { 'bar': 'hello' } }); 866 | * }); 867 | * 868 | * structure.cursor(['foo', 'bar']).update(function () { 869 | * return 'bye'; 870 | * }); 871 | * ``` 872 | * 873 | * But for `change` 874 | * ```js 875 | * var structure = new Structure({ 'foo': { 'bar': 'hello' } }); 876 | * 877 | * structure.on('change', function (newData, oldData, keyPath) { 878 | * keyPath.should.eql(['foo', 'bar']); 879 | * newData.should.eql('bye'); 880 | * oldData.should.eql('hello'); 881 | * }); 882 | * 883 | * structure.cursor(['foo', 'bar']).update(function () { 884 | * return 'bye'; 885 | * }); 886 | * ``` 887 | * 888 | * **All `keyPath`s passed to listeners are the full path to where the actual 889 | * change happened** 890 | * 891 | * ### Examples: 892 | * ```js 893 | * var Structure = require('immstruct/structure'); 894 | * var s = new Structure({ data: { foo: 'bar' }}); 895 | * 896 | * // Or: 897 | * // var Structure = require('immstruct').Structure; 898 | * ``` 899 | * 900 | * ### Events 901 | * 902 | * * `swap`: Emitted when cursor is updated (new information is set). Is emitted 903 | * on all types of changes, additions and deletions. The passed structures are 904 | * always the root structure. 905 | * One use case for this is to re-render design components. Callback 906 | * is passed arguments: `newStructure`, `oldStructure`, `keyPath`. 907 | * * `next-animation-frame`: Same as `swap`, but only emitted on animation frame. 908 | * Could use with many render updates and better performance. Callback is passed 909 | * arguments: `newStructure`, `oldStructure`, `keyPath`. 910 | * * `change`: Emitted when data/value is updated and it existed before. Emits 911 | * values: `newValue`, `oldValue` and `path`. 912 | * * `delete`: Emitted when data/value is removed. Emits value: `removedValue` and `path`. 913 | * * `add`: Emitted when new data/value is added. Emits value: `newValue` and `path`. 914 | * * `any`: With the same semantics as `add`, `change` or `delete`, `any` is triggered for 915 | * all types of changes. Differs from swap in the arguments that it is passed. 916 | * Is passed `newValue` (or undefined), `oldValue` (or undefined) and full `keyPath`. 917 | * New and old value are the changed value, not relative/scoped to the reference path as 918 | * with `swap`. 919 | * 920 | * ### Options 921 | * 922 | * ```json 923 | * { 924 | * key: String, // Defaults to random string 925 | * data: Object|Immutable, // defaults to empty Map 926 | * history: Boolean, // Defaults to false 927 | * historyLimit: Number, // If history enabled, Defaults to Infinity 928 | * } 929 | * ``` 930 | * 931 | * @property {Immutable.List} history `Immutable.List` with history. 932 | * @property {Object|Immutable} current Provided data as immutable data 933 | * @property {String} key Generated or provided key. 934 | * 935 | * 936 | * @param {{ key: String, data: Object, history: Boolean }} [options] - defaults 937 | * to random key and empty data (immutable structure). No history 938 | * 939 | * @constructor 940 | * @class {Structure} 941 | * @returns {Structure} 942 | * @api public 943 | */ 944 | function Structure (options) { 945 | var self = this; 946 | 947 | options = options || {}; 948 | if (!(this instanceof Structure)) { 949 | return new Structure(options); 950 | } 951 | 952 | this.key = options.key || utils.generateRandomKey(); 953 | 954 | this._queuedChange = false; 955 | this.current = options.data; 956 | if (!isImmutableStructure(this.current) || !this.current) { 957 | this.current = Immutable.fromJS(this.current || {}); 958 | } 959 | 960 | if (!!options.history) { 961 | this.history = Immutable.List.of(this.current); 962 | this._currentRevision = 0; 963 | this._historyLimit = (typeof options.historyLimit === 'number') ? 964 | options.historyLimit : 965 | Infinity; 966 | } 967 | 968 | this._referencelisteners = Immutable.Map(); 969 | this.on('swap', function (newData, oldData, keyPath) { 970 | keyPath = keyPath || []; 971 | var args = [newData, oldData, keyPath]; 972 | emit(self._referencelisteners, newData, oldData, keyPath, args); 973 | }); 974 | 975 | EventEmitter.call(this, arguments); 976 | } 977 | inherits(Structure, EventEmitter); 978 | module.exports = Structure; 979 | 980 | function emit(map, newData, oldData, path, args) { 981 | if (!map || newData === oldData) return void 0; 982 | map.get(LISTENER_SENTINEL, []).forEach(function (fn) { 983 | fn.apply(null, args); 984 | }); 985 | 986 | if (path.length > 0) { 987 | var nextPathRoot = path[0]; 988 | var passedNewData = newData && newData.get ? newData.get(nextPathRoot) : void 0; 989 | var passedOldData = oldData && oldData.get ? oldData.get(nextPathRoot) : void 0; 990 | return emit(map.get(nextPathRoot), passedNewData, passedOldData, path.slice(1), args); 991 | } 992 | 993 | map.forEach(function(value, key) { 994 | if (key === LISTENER_SENTINEL) return void 0; 995 | var passedNewData = (newData && newData.get) ? newData.get(key) : void 0; 996 | var passedOldData = (oldData && oldData.get) ? oldData.get(key) : void 0; 997 | emit(value, passedNewData, passedOldData, [], args); 998 | }); 999 | } 1000 | 1001 | /** 1002 | * Create a Immutable.js Cursor for a given `path` on the `current` structure (see `Structure.current`). 1003 | * Changes made through created cursor will cause a `swap` event to happen (see `Events`). 1004 | * 1005 | * **This method returns a 1006 | * [Immutable.js Cursor](https://github.com/facebook/immutable-js/blob/master/contrib/cursor/index.d.ts). 1007 | * See the Immutable.js docs for more info on how to use cursors.** 1008 | * 1009 | * ### Examples: 1010 | * ```js 1011 | * var Structure = require('immstruct/structure'); 1012 | * var s = new Structure({ data: { foo: 'bar', a: { b: 'foo' } }}); 1013 | * s.cursor().set('foo', 'hello'); 1014 | * s.cursor('foo').update(function () { return 'Changed'; }); 1015 | * s.cursor(['a', 'b']).update(function () { return 'bar'; }); 1016 | * ``` 1017 | * 1018 | * See more examples in the [tests](https://github.com/omniscientjs/immstruct/blob/master/tests/structure_test.js) 1019 | * 1020 | * @param {String|Array} [path] - defaults to empty string. Can be array for path. See Immutable.js Cursors 1021 | * 1022 | * @api public 1023 | * @module structure.cursor 1024 | * @returns {Cursor} Gives a Cursor from Immutable.js 1025 | */ 1026 | Structure.prototype.cursor = function (path) { 1027 | var self = this; 1028 | path = valToKeyPath(path) || []; 1029 | 1030 | if (!this.current) { 1031 | throw new Error('No structure loaded.'); 1032 | } 1033 | 1034 | var changeListener = function (newRoot, oldRoot, path) { 1035 | if(self.current === oldRoot) { 1036 | self.current = newRoot; 1037 | } else if(!hasIn(newRoot, path)) { 1038 | // Othewise an out-of-sync change occured. We ignore `oldRoot`, and focus on 1039 | // changes at path `path`, and sync this to `self.current`. 1040 | self.current = self.current.removeIn(path); 1041 | } else { 1042 | // Update an existing path or add a new path within the current map. 1043 | self.current = self.current.setIn(path, newRoot.getIn(path)); 1044 | } 1045 | 1046 | return self.current; 1047 | }; 1048 | 1049 | changeListener = handleHistory(this, changeListener); 1050 | changeListener = handleSwap(this, changeListener); 1051 | changeListener = handlePersisting(this, changeListener); 1052 | return Cursor.from(self.current, path, changeListener); 1053 | }; 1054 | 1055 | /** 1056 | * Creates a reference. A reference can be a pointer to a cursor, allowing 1057 | * you to create cursors for a specific path any time. This is essentially 1058 | * a way to have "always updated cursors" or Reference Cursors. See example 1059 | * for better understanding the concept. 1060 | * 1061 | * References also allow you to listen for changes specific for a path. 1062 | * 1063 | * ### Examples: 1064 | * ```js 1065 | * var structure = immstruct({ 1066 | * someBox: { message: 'Hello World!' } 1067 | * }); 1068 | * var ref = structure.reference(['someBox']); 1069 | * 1070 | * var unobserve = ref.observe(function () { 1071 | * // Called when data the path 'someBox' is changed. 1072 | * // Also called when the data at ['someBox', 'message'] is changed. 1073 | * }); 1074 | * 1075 | * // Update the data using the ref 1076 | * ref.cursor().update(function () { return 'updated'; }); 1077 | * 1078 | * // Update the data using the initial structure 1079 | * structure.cursor(['someBox', 'message']).update(function () { return 'updated again'; }); 1080 | * 1081 | * // Remove the listener 1082 | * unobserve(); 1083 | * ``` 1084 | * 1085 | * See more examples in the [readme](https://github.com/omniscientjs/immstruct) 1086 | * 1087 | * @param {String|Array|Cursor} [path|cursor] - defaults to empty string. Can be 1088 | * array for path or use path of cursor. See Immutable.js Cursors 1089 | * 1090 | * @api public 1091 | * @module structure.reference 1092 | * @returns {Reference} 1093 | * @constructor 1094 | */ 1095 | Structure.prototype.reference = function reference (path) { 1096 | if (isCursor(path) && path._keyPath) { 1097 | path = path._keyPath; 1098 | } 1099 | 1100 | path = valToKeyPath(path) || []; 1101 | 1102 | var self = this, 1103 | cursor = this.cursor(path), 1104 | unobservers = Immutable.Set(); 1105 | 1106 | function cursorRefresher() { cursor = self.cursor(path); } 1107 | function _subscribe (path, fn) { 1108 | self._referencelisteners = subscribe(self._referencelisteners, path, fn); 1109 | } 1110 | function _unsubscribe (path, fn) { 1111 | self._referencelisteners = unsubscribe(self._referencelisteners, path, fn); 1112 | } 1113 | 1114 | _subscribe(path, cursorRefresher); 1115 | 1116 | return { 1117 | /** 1118 | * Observe for changes on a reference. On references you can observe for changes, 1119 | * but a reference **is not** an EventEmitter it self. 1120 | * 1121 | * The passed `keyPath` for swap events are relative to the reference, but 1122 | * 1123 | * 1124 | * **Note**: As on `swap` for normal immstruct events, the passed arguments for 1125 | * the event is the root, not guaranteed to be the actual changed value. 1126 | * The structure is how ever scoped to the path passed in to the reference. 1127 | * All values passed to the eventlistener for the swap event are relative 1128 | * to the path used as key path to the reference. 1129 | * 1130 | * For instance: 1131 | * 1132 | * ```js 1133 | * var structure = immstruct({ 'foo': { 'bar': 'hello' } }); 1134 | * var ref = structure.reference('foo'); 1135 | * ref.observe(function (newData, oldData, keyPath) { 1136 | * keyPath.should.eql(['bar']); 1137 | * newData.toJS().should.eql({ 'bar': 'updated' }); 1138 | * oldData.toJS().should.eql({ 'bar': 'hello' }); 1139 | * }); 1140 | * ref.cursor().update(['bar'], function () { return 'updated'; }); 1141 | * ``` 1142 | * 1143 | * For type specific events, how ever, the actual changed value is passed, 1144 | * not the root data. In these cases, the full keyPath to the change is passed. 1145 | * 1146 | * For instance: 1147 | * 1148 | * ```js 1149 | * var structure = immstruct({ 'foo': { 'bar': 'hello' } }); 1150 | * var ref = structure.reference('foo'); 1151 | * ref.observe('change', function (newValue, oldValue, keyPath) { 1152 | * keyPath.should.eql(['foo', 'bar']); 1153 | * newData.should.eql('updated'); 1154 | * oldData.should.eql('hello'); 1155 | * }); 1156 | * ref.cursor().update(['bar'], function () { return 'updated'; }); 1157 | * ``` 1158 | * 1159 | * 1160 | * ### Examples: 1161 | * ```js 1162 | * var ref = structure.reference(['someBox']); 1163 | * 1164 | * var unobserve = ref.observe('delete', function () { 1165 | * // Called when data the path 'someBox' is removed from the structure. 1166 | * }); 1167 | * ``` 1168 | * 1169 | * See more examples in the [readme](https://github.com/omniscientjs/immstruct) 1170 | * 1171 | * ### Events 1172 | * * `swap`: Emitted when any cursor is updated (new information is set). 1173 | * Triggered in any data swap is made on the structure. One use case for 1174 | * this is to re-render design components. Data passed as arguments 1175 | * are scoped/relative to the path passed to the reference, this also goes for keyPath. 1176 | * Callback is passed arguments: `newStructure`, `oldStructure`, `keyPath`. 1177 | * * `change`: Emitted when data/value is updated and it existed before. 1178 | * Emits values: `newValue`, `oldValue` and `path`. 1179 | * * `delete`: Emitted when data/value is removed. Emits value: `removedValue` and `path`. 1180 | * * `add`: Emitted when new data/value is added. Emits value: `newValue` and `path`. 1181 | * * `any`: With the same semantics as `add`, `change` or `delete`, `any` is triggered for 1182 | * all types of changes. Differs from swap in the arguments that it is passed. 1183 | * Is passed `newValue` (or undefined), `oldValue` (or undefined) and full `keyPath`. 1184 | * New and old value are the changed value, not relative/scoped to the reference path as 1185 | * with `swap`. 1186 | * 1187 | * @param {String} [eventName] - Type of change 1188 | * @param {Function} callback - Callback when referenced data is swapped 1189 | * 1190 | * @api public 1191 | * @module reference.observe 1192 | * @returns {Function} Function for removing observer (unobserve) 1193 | */ 1194 | observe: function (eventName, newFn) { 1195 | if (typeof eventName === 'function') { 1196 | newFn = eventName; 1197 | eventName = void 0; 1198 | } 1199 | if (this._dead || typeof newFn !== 'function') return; 1200 | if (eventName && eventName !== 'swap') { 1201 | newFn = onEventNameAndAny(eventName, newFn); 1202 | } else { 1203 | newFn = emitScopedReferencedStructures(path, newFn); 1204 | } 1205 | 1206 | _subscribe(path, newFn); 1207 | unobservers = unobservers.add(newFn); 1208 | 1209 | return function unobserve () { 1210 | _unsubscribe(path, newFn); 1211 | }; 1212 | }, 1213 | 1214 | /** 1215 | * Create a new, updated, cursor from the base path provded to the 1216 | * reference. This returns a Immutable.js Cursor as the regular 1217 | * cursor method. You can also provide a sub-path to create a reference 1218 | * in a deeper level. 1219 | * 1220 | * ### Examples: 1221 | * ```js 1222 | * var ref = structure.reference(['someBox']); 1223 | * var cursor = ref.cursor('someSubPath'); 1224 | * var cursor2 = ref.cursor(); 1225 | * ``` 1226 | * 1227 | * See more examples in the [readme](https://github.com/omniscientjs/immstruct) 1228 | * 1229 | * @param {String} [subpath] - Subpath to a deeper structure 1230 | * 1231 | * @api public 1232 | * @module reference.cursor 1233 | * @returns {Cursor} Immutable.js cursor 1234 | */ 1235 | cursor: function (subPath) { 1236 | if (this._dead) return void 0; 1237 | subPath = valToKeyPath(subPath); 1238 | if (subPath) return cursor.cursor(subPath); 1239 | return cursor; 1240 | }, 1241 | 1242 | /** 1243 | * Creates a reference on a lower level path. See creating normal references. 1244 | * 1245 | * ### Examples: 1246 | * ```js 1247 | * var structure = immstruct({ 1248 | * someBox: { message: 'Hello World!' } 1249 | * }); 1250 | * var ref = structure.reference('someBox'); 1251 | * 1252 | * var newReference = ref.reference('message'); 1253 | * ``` 1254 | * 1255 | * See more examples in the [readme](https://github.com/omniscientjs/immstruct) 1256 | * 1257 | * @param {String|Array} [path] - defaults to empty string. Can be array for path. See Immutable.js Cursors 1258 | * 1259 | * @api public 1260 | * @see structure.reference 1261 | * @module reference.reference 1262 | * @returns {Reference} 1263 | */ 1264 | reference: function (subPath) { 1265 | subPath = valToKeyPath(subPath); 1266 | return self.reference((cursor._keyPath || []).concat(subPath)); 1267 | }, 1268 | 1269 | /** 1270 | * Remove all observers from reference. 1271 | * 1272 | * @api public 1273 | * @module reference.unobserveAll 1274 | * @returns {Void} 1275 | */ 1276 | unobserveAll: function (destroy) { 1277 | if (this._dead) return void 0; 1278 | unobservers.forEach(function(fn) { 1279 | _unsubscribe(path, fn); 1280 | }); 1281 | 1282 | if (destroy) { 1283 | _unsubscribe(path, cursorRefresher); 1284 | } 1285 | }, 1286 | 1287 | /** 1288 | * Destroy reference. Unobserve all observers, set all endpoints of reference to dead. 1289 | * For cleaning up memory. 1290 | * 1291 | * @api public 1292 | * @module reference.destroy 1293 | * @returns {Void} 1294 | */ 1295 | destroy: function () { 1296 | cursor = void 0; 1297 | this.unobserveAll(true); 1298 | 1299 | this._dead = true; 1300 | this.observe = void 0; 1301 | this.unobserveAll = void 0; 1302 | this.cursor = void 0; 1303 | this.destroy = void 0; 1304 | 1305 | cursorRefresher = void 0; 1306 | _unsubscribe = void 0; 1307 | _subscribe = void 0; 1308 | } 1309 | }; 1310 | }; 1311 | 1312 | /** 1313 | * Force emitting swap event. Pass on new, old and keypath passed to swap. 1314 | * If newData is `null` current will be used. 1315 | * 1316 | * @param {Object} newData - Immutable object for the new data to emit 1317 | * @param {Object} oldData - Immutable object for the old data to emit 1318 | * @param {String} keyPath - Structure path (in tree) to where the changes occured. 1319 | * 1320 | * @api public 1321 | * @module structure.forceHasSwapped 1322 | * @returns {Void} 1323 | */ 1324 | Structure.prototype.forceHasSwapped = function (newData, oldData, keyPath) { 1325 | this.emit('swap', newData || this.current, oldData, keyPath); 1326 | possiblyEmitAnimationFrameEvent(this, newData || this.current, oldData, keyPath); 1327 | }; 1328 | 1329 | 1330 | /** 1331 | * Undo IFF history is activated and there are steps to undo. Returns new current 1332 | * immutable structure. 1333 | * 1334 | * **Will NOT emit swap when redo. You have to do this yourself**. 1335 | * 1336 | * Define number of steps to undo in param. 1337 | * 1338 | * @param {Number} steps - Number of steps to undo 1339 | * 1340 | * @api public 1341 | * @module structure.undo 1342 | * @returns {Object} New Immutable structure after undo 1343 | */ 1344 | Structure.prototype.undo = function(steps) { 1345 | this._currentRevision -= steps || 1; 1346 | if (this._currentRevision < 0) { 1347 | this._currentRevision = 0; 1348 | } 1349 | 1350 | this.current = this.history.get(this._currentRevision); 1351 | return this.current; 1352 | }; 1353 | 1354 | /** 1355 | * Redo IFF history is activated and you can redo. Returns new current immutable structure. 1356 | * Define number of steps to redo in param. 1357 | * **Will NOT emit swap when redo. You have to do this yourself**. 1358 | * 1359 | * @param {Number} head - Number of steps to head to in redo 1360 | * 1361 | * @api public 1362 | * @module structure.redo 1363 | * @returns {Object} New Immutable structure after redo 1364 | */ 1365 | Structure.prototype.redo = function(head) { 1366 | this._currentRevision += head || 1; 1367 | if (this._currentRevision > this.history.count() - 1) { 1368 | this._currentRevision = this.history.count() - 1; 1369 | } 1370 | 1371 | this.current = this.history.get(this._currentRevision); 1372 | return this.current; 1373 | }; 1374 | 1375 | /** 1376 | * Undo IFF history is activated and passed `structure` exists in history. 1377 | * Returns the same immutable structure as passed as argument. 1378 | * 1379 | * **Will NOT emit swap after undo. You have to do this yourself**. 1380 | * 1381 | * @param {Object} structure - Immutable structure to redo until 1382 | * 1383 | * @api public 1384 | * @module structure.undoUntil 1385 | * @returns {Object} New Immutable structure after undo 1386 | */ 1387 | Structure.prototype.undoUntil = function(structure) { 1388 | this._currentRevision = this.history.indexOf(structure); 1389 | this.current = structure; 1390 | 1391 | return structure; 1392 | }; 1393 | 1394 | 1395 | function subscribe(listeners, path, fn) { 1396 | return listeners.updateIn(path.concat(LISTENER_SENTINEL), Immutable.OrderedSet(), function(old) { 1397 | return old.add(fn); 1398 | }); 1399 | } 1400 | 1401 | function unsubscribe(listeners, path, fn) { 1402 | return listeners.updateIn(path.concat(LISTENER_SENTINEL), Immutable.OrderedSet(), function(old) { 1403 | return old.remove(fn); 1404 | }); 1405 | } 1406 | 1407 | // Private decorators. 1408 | 1409 | // Update history if history is active 1410 | function handleHistory (emitter, fn) { 1411 | return function handleHistoryFunction (newData, oldData, path) { 1412 | var newStructure = fn.apply(fn, arguments); 1413 | if (!emitter.history || (newData === oldData)) return newStructure; 1414 | 1415 | emitter.history = emitter.history 1416 | .take(++emitter._currentRevision) 1417 | .push(emitter.current); 1418 | 1419 | if (emitter.history.size > emitter._historyLimit) { 1420 | emitter.history = emitter.history.takeLast(emitter._historyLimit); 1421 | emitter._currentRevision -= (emitter.history.size - emitter._historyLimit); 1422 | } 1423 | 1424 | return newStructure; 1425 | }; 1426 | } 1427 | 1428 | var _requestAnimationFrame = (typeof window !== 'undefined' && 1429 | window.requestAnimationFrame) || utils.raf; 1430 | 1431 | // Update history if history is active 1432 | function possiblyEmitAnimationFrameEvent (emitter, newStructure, oldData, keyPath) { 1433 | if (emitter._queuedChange) return void 0; 1434 | emitter._queuedChange = true; 1435 | 1436 | _requestAnimationFrame(function () { 1437 | emitter._queuedChange = false; 1438 | emitter.emit('next-animation-frame', newStructure, oldData, keyPath); 1439 | }); 1440 | } 1441 | 1442 | // Emit swap event on values are swapped 1443 | function handleSwap (emitter, fn) { 1444 | return function handleSwapFunction (newData, oldData, keyPath) { 1445 | var previous = emitter.current; 1446 | var newStructure = fn.apply(fn, arguments); 1447 | if(newData === previous) return newStructure; 1448 | 1449 | emitter.emit('swap', newStructure, previous, keyPath); 1450 | possiblyEmitAnimationFrameEvent(emitter, newStructure, previous, keyPath); 1451 | 1452 | return newStructure; 1453 | }; 1454 | } 1455 | 1456 | // Map changes to update events (delete/change/add). 1457 | function handlePersisting (emitter, fn) { 1458 | return function handlePersistingFunction (newData, oldData, path) { 1459 | var previous = emitter.current; 1460 | var newStructure = fn.apply(fn, arguments); 1461 | if(newData === previous) return newStructure; 1462 | var info = analyze(newData, previous, path); 1463 | 1464 | if (info.eventName) { 1465 | emitter.emit.apply(emitter, [info.eventName].concat(info.args)); 1466 | emitter.emit('any', info.newObject, info.oldObject, path); 1467 | } 1468 | return newStructure; 1469 | }; 1470 | } 1471 | 1472 | // Private helpers. 1473 | 1474 | function analyze (newData, oldData, path) { 1475 | var oldObject = oldData && oldData.getIn(path); 1476 | var newObject = newData && newData.getIn(path); 1477 | 1478 | var inOld = oldData && hasIn(oldData, path); 1479 | var inNew = newData && hasIn(newData, path); 1480 | 1481 | var args, eventName; 1482 | 1483 | if (inOld && !inNew) { 1484 | eventName = 'delete'; 1485 | args = [oldObject, path]; 1486 | } else if (inOld && inNew) { 1487 | eventName = 'change'; 1488 | args = [newObject, oldObject, path]; 1489 | } else if (!inOld && inNew) { 1490 | eventName = 'add'; 1491 | args = [newObject, path]; 1492 | } 1493 | 1494 | return { 1495 | eventName: eventName, 1496 | args: args, 1497 | newObject: newObject, 1498 | oldObject: oldObject 1499 | }; 1500 | } 1501 | 1502 | // Check if path exists. 1503 | var NOT_SET = {}; 1504 | function hasIn(cursor, path) { 1505 | if(cursor.hasIn) return cursor.hasIn(path); 1506 | return cursor.getIn(path, NOT_SET) !== NOT_SET; 1507 | } 1508 | 1509 | function onEventNameAndAny(eventName, fn) { 1510 | return function (newData, oldData, keyPath) { 1511 | var info = analyze(newData, oldData, keyPath); 1512 | 1513 | if (info.eventName !== eventName && eventName !== 'any') return void 0; 1514 | if (eventName === 'any') { 1515 | return fn.call(fn, info.newObject, info.oldObject, keyPath); 1516 | } 1517 | return fn.apply(fn, info.args); 1518 | }; 1519 | } 1520 | 1521 | function emitScopedReferencedStructures(path, fn) { 1522 | return function withReferenceScopedStructures (newStructure, oldStructure, keyPath) { 1523 | return fn.call(this, newStructure.getIn(path), oldStructure.getIn(path), keyPath.slice(path.length)); 1524 | }; 1525 | } 1526 | 1527 | function isCursor (potential) { 1528 | return potential && typeof potential.deref === 'function'; 1529 | } 1530 | 1531 | // Check if passed structure is existing immutable structure. 1532 | // From https://github.com/facebook/immutable-js/wiki/Upgrading-to-Immutable-v3#additional-changes 1533 | var immutableCheckers = [ 1534 | {name: 'Iterable', method: 'isIterable' }, 1535 | {name: 'Seq', method: 'isSeq'}, 1536 | {name: 'Map', method: 'isMap'}, 1537 | {name: 'OrderedMap', method: 'isOrderedMap'}, 1538 | {name: 'List', method: 'isList'}, 1539 | {name: 'Stack', method: 'isStack'}, 1540 | {name: 'Set', method: 'isSet'} 1541 | ]; 1542 | function isImmutableStructure (data) { 1543 | return immutableCheckers.some(function (checkItem) { 1544 | return immutableSafeCheck(checkItem.name, checkItem.method, data); 1545 | }); 1546 | } 1547 | 1548 | function immutableSafeCheck (ns, method, data) { 1549 | return Immutable[ns] && Immutable[ns][method] && Immutable[ns][method](data); 1550 | } 1551 | 1552 | function valToKeyPath(val) { 1553 | if (typeof val === 'undefined') { 1554 | return val; 1555 | } 1556 | return Array.isArray(val) ? val : 1557 | immutableSafeCheck('Iterable', 'isIterable', val) ? 1558 | val.toArray() : [val]; 1559 | } 1560 | 1561 | function inherits (c, p) { 1562 | var e = {}; 1563 | Object.getOwnPropertyNames(c.prototype).forEach(function (k) { 1564 | e[k] = Object.getOwnPropertyDescriptor(c.prototype, k); 1565 | }); 1566 | c.prototype = Object.create(p.prototype, e); 1567 | c['super'] = p; 1568 | } 1569 | 1570 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 1571 | },{"./utils":5,"eventemitter3":2,"immutable/contrib/cursor/index":3}],5:[function(_dereq_,module,exports){ 1572 | 'use strict'; 1573 | 1574 | module.exports.generateRandomKey = function (len) { 1575 | len = len || 10; 1576 | return Math.random().toString(36).substring(2).substring(0, len); 1577 | }; 1578 | 1579 | // Variation shim based on the classic polyfill: 1580 | // http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ 1581 | module.exports.raf = (function() { 1582 | var glob = (typeof window === 'undefined') ? module : window; 1583 | var lastTime = 0; 1584 | var vendors = ['webkit', 'moz']; 1585 | for(var x = 0; x < vendors.length && !glob.requestAnimationFrame; ++x) { 1586 | glob.requestAnimationFrame = glob[vendors[x]+'RequestAnimationFrame']; 1587 | } 1588 | 1589 | return function(callback, element) { 1590 | var currTime = new Date().getTime(); 1591 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 1592 | var id = setTimeout(function() { callback(currTime + timeToCall); }, 1593 | timeToCall); 1594 | lastTime = currTime + timeToCall; 1595 | return id; 1596 | }; 1597 | }()); 1598 | 1599 | },{}]},{},[1])(1) 1600 | }); -------------------------------------------------------------------------------- /dist/immstruct.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * immstruct v2.0.0 3 | * A part of the Omniscient.js project 4 | ***************************************/ 5 | !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.immstruct=t()}}(function(){return function t(e,n,r){function i(u,s){if(!n[u]){if(!e[u]){var a="function"==typeof require&&require;if(!s&&a)return a(u,!0);if(o)return o(u,!0);var c=new Error("Cannot find module '"+u+"'");throw c.code="MODULE_NOT_FOUND",c}var f=n[u]={exports:{}};e[u][0].call(f.exports,function(t){var n=e[u][1][t];return i(n?n:t)},f,f.exports,t,e,n,r)}return n[u].exports}for(var o="function"==typeof require&&require,u=0;ui;i++)s[i]=r[i].fn;return s},i.prototype.emit=function(t,e,n,r,i,u){var s=o?o+t:t;if(!this._events||!this._events[s])return!1;var a,c,f=this._events[s],h=arguments.length;if("function"==typeof f.fn){switch(f.once&&this.removeListener(t,f.fn,void 0,!0),h){case 1:return f.fn.call(f.context),!0;case 2:return f.fn.call(f.context,e),!0;case 3:return f.fn.call(f.context,e,n),!0;case 4:return f.fn.call(f.context,e,n,r),!0;case 5:return f.fn.call(f.context,e,n,r,i),!0;case 6:return f.fn.call(f.context,e,n,r,i,u),!0}for(c=1,a=new Array(h-1);h>c;c++)a[c-1]=arguments[c];f.fn.apply(f.context,a)}else{var p,d=f.length;for(c=0;d>c;c++)switch(f[c].once&&this.removeListener(t,f[c].fn,void 0,!0),h){case 1:f[c].fn.call(f[c].context);break;case 2:f[c].fn.call(f[c].context,e);break;case 3:f[c].fn.call(f[c].context,e,n);break;default:if(!a)for(p=1,a=new Array(h-1);h>p;p++)a[p-1]=arguments[p];f[c].fn.apply(f[c].context,a)}}return!0},i.prototype.on=function(t,e,n){var i=new r(e,n||this),u=o?o+t:t;return this._events||(this._events=o?{}:Object.create(null)),this._events[u]?this._events[u].fn?this._events[u]=[this._events[u],i]:this._events[u].push(i):this._events[u]=i,this},i.prototype.once=function(t,e,n){var i=new r(e,n||this,!0),u=o?o+t:t;return this._events||(this._events=o?{}:Object.create(null)),this._events[u]?this._events[u].fn?this._events[u]=[this._events[u],i]:this._events[u].push(i):this._events[u]=i,this},i.prototype.removeListener=function(t,e,n,r){var i=o?o+t:t;if(!this._events||!this._events[i])return this;var u=this._events[i],s=[];if(e)if(u.fn)(u.fn!==e||r&&!u.once||n&&u.context!==n)&&s.push(u);else for(var a=0,c=u.length;c>a;a++)(u[a].fn!==e||r&&!u[a].once||n&&u[a].context!==n)&&s.push(u[a]);return s.length?this._events[i]=1===s.length?s[0]:s:delete this._events[i],this},i.prototype.removeAllListeners=function(t){return this._events?(t?delete this._events[o?o+t:t]:this._events=o?{}:Object.create(null),this):this},i.prototype.off=i.prototype.removeListener,i.prototype.addListener=i.prototype.on,i.prototype.setMaxListeners=function(){return this},i.prefixed=o,"undefined"!=typeof e&&(e.exports=i)},{}],3:[function(t,e,n){function r(t,e,n){return 1===arguments.length?e=[]:"function"==typeof e?(n=e,e=[]):e=v(e),u(t,e,n)}function i(t,e,n,r){this.size=r,this._rootData=t,this._keyPath=e,this._onChange=n}function o(t,e,n,r){this.size=r,this._rootData=t,this._keyPath=e,this._onChange=n}function u(t,e,n,r){arguments.length<4&&(r=t.getIn(e));var u=r&&r.size,a=l.isIndexed(r)?o:i,c=new a(t,e,n,u);return r instanceof w&&s(c,r),c}function s(t,e){try{e._keys.forEach(a.bind(void 0,t))}catch(n){}}function a(t,e){Object.defineProperty(t,e,{get:function(){return this.get(e)},set:function(t){if(!this.__ownerID)throw new Error("Cannot set on an immutable record.")}})}function c(t,e,n){return l.isIterable(n)?f(t,e,n):n}function f(t,e,n){return arguments.length<3?u(t._rootData,p(t._keyPath,e),t._onChange):u(t._rootData,p(t._keyPath,e),t._onChange,n)}function h(t,e,n){var r=arguments.length>2,i=t._rootData.updateIn(t._keyPath,r?g():void 0,e),o=t._keyPath||[],s=t._onChange&&t._onChange.call(void 0,i,t._rootData,r?p(o,n):o);return void 0!==s&&(i=s),u(i,t._keyPath,t._onChange)}function p(t,e){return t.concat(d(e))}function d(t){return Array.isArray(t)?t:y.Iterable(t).toArray()}function v(t){return Array.isArray(t)?t:l.isIterable(t)?t.toArray():[t]}var y="undefined"!=typeof window?window.Immutable:"undefined"!=typeof global?global.Immutable:null,l=y.Iterable,m=l.Iterator,_=y.Seq,g=y.Map,w=y.Record,b=Object.create(_.Keyed.prototype),I=Object.create(_.Indexed.prototype);b.constructor=i,I.constructor=o,b.toString=function(){return this.__toString("Cursor {","}")},I.toString=function(){return this.__toString("Cursor [","]")},b.deref=b.valueOf=I.deref=I.valueOf=function(t){return this._rootData.getIn(this._keyPath,t)},b.get=I.get=function(t,e){return this.getIn([t],e)},b.getIn=I.getIn=function(t,e){if(t=d(t),0===t.length)return this;var n=this._rootData.getIn(p(this._keyPath,t),x);return n===x?e:c(this,t,n)},I.set=b.set=function(t,e){return h(this,function(n){return n.set(t,e)},[t])},I.push=function(){var t=arguments;return h(this,function(e){return e.push.apply(e,t)})},I.pop=function(){return h(this,function(t){return t.pop()})},I.unshift=function(){var t=arguments;return h(this,function(e){return e.unshift.apply(e,t)})},I.shift=function(){return h(this,function(t){return t.shift()})},I.setIn=b.setIn=g.prototype.setIn,b.remove=b["delete"]=I.remove=I["delete"]=function(t){return h(this,function(e){return e.remove(t)},[t])},I.removeIn=I.deleteIn=b.removeIn=b.deleteIn=g.prototype.deleteIn,b.clear=I.clear=function(){return h(this,function(t){return t.clear()})},I.update=b.update=function(t,e,n){return 1===arguments.length?h(this,t):this.updateIn([t],e,n)},I.updateIn=b.updateIn=function(t,e,n){return h(this,function(r){return r.updateIn(t,e,n)},t)},I.merge=b.merge=function(){var t=arguments;return h(this,function(e){return e.merge.apply(e,t)})},I.mergeWith=b.mergeWith=function(t){var e=arguments;return h(this,function(t){return t.mergeWith.apply(t,e)})},I.mergeIn=b.mergeIn=g.prototype.mergeIn,I.mergeDeep=b.mergeDeep=function(){var t=arguments;return h(this,function(e){return e.mergeDeep.apply(e,t)})},I.mergeDeepWith=b.mergeDeepWith=function(t){var e=arguments;return h(this,function(t){return t.mergeDeepWith.apply(t,e)})},I.mergeDeepIn=b.mergeDeepIn=g.prototype.mergeDeepIn,b.withMutations=I.withMutations=function(t){return h(this,function(e){return(e||g()).withMutations(t)})},b.cursor=I.cursor=function(t){return t=v(t),0===t.length?this:f(this,t)},b.__iterate=I.__iterate=function(t,e){var n=this,r=n.deref();return r&&r.__iterate?r.__iterate(function(e,r){return t(c(n,[r],e),r,n)},e):0},b.__iterator=I.__iterator=function(t,e){var n=this.deref(),r=this,i=n&&n.__iterator&&n.__iterator(m.ENTRIES,e);return new m(function(){if(!i)return{value:void 0,done:!0};var e=i.next();if(e.done)return e;var n=e.value,o=n[0],u=c(r,[o],n[1]);return{value:t===m.KEYS?o:t===m.VALUES?u:[o,u],done:!1}})},i.prototype=b,o.prototype=I;var x={};n.from=r},{immutable:void 0}],4:[function(t,e,n){(function(n){"use strict";function r(t){var e=this;return t=t||{},this instanceof r?(this.key=t.key||x.generateRandomKey(),this._queuedChange=!1,this.current=t.data,l(this.current)&&this.current||(this.current=w.fromJS(this.current||{})),t.history&&(this.history=w.List.of(this.current),this._currentRevision=0,this._historyLimit="number"==typeof t.historyLimit?t.historyLimit:1/0),this._referencelisteners=w.Map(),this.on("swap",function(t,n,r){r=r||[];var o=[t,n,r];i(e._referencelisteners,t,n,r,o)}),void I.call(this,arguments)):new r(t)}function i(t,e,n,r,o){if(!t||e===n)return void 0;if(t.get(k,[]).forEach(function(t){t.apply(null,o)}),r.length>0){var u=r[0],s=e&&e.get?e.get(u):void 0,a=n&&n.get?n.get(u):void 0;return i(t.get(u),s,a,r.slice(1),o)}t.forEach(function(t,r){if(r===k)return void 0;var u=e&&e.get?e.get(r):void 0,s=n&&n.get?n.get(r):void 0;i(t,u,s,[],o)})}function o(t,e,n){return t.updateIn(e.concat(k),w.OrderedSet(),function(t){return t.add(n)})}function u(t,e,n){return t.updateIn(e.concat(k),w.OrderedSet(),function(t){return t.remove(n)})}function s(t,e){return function(n,r,i){var o=e.apply(e,arguments);return t.history&&n!==r?(t.history=t.history.take(++t._currentRevision).push(t.current),t.history.size>t._historyLimit&&(t.history=t.history.takeLast(t._historyLimit),t._currentRevision-=t.history.size-t._historyLimit),o):o}}function a(t,e,n,r){return t._queuedChange?void 0:(t._queuedChange=!0,void O(function(){t._queuedChange=!1,t.emit("next-animation-frame",e,n,r)}))}function c(t,e){return function(n,r,i){var o=t.current,u=e.apply(e,arguments);return n===o?u:(t.emit("swap",u,o,i),a(t,u,o,i),u)}}function f(t,e){return function(n,r,i){var o=t.current,u=e.apply(e,arguments);if(n===o)return u;var s=h(n,o,i);return s.eventName&&(t.emit.apply(t,[s.eventName].concat(s.args)),t.emit("any",s.newObject,s.oldObject,i)),u}}function h(t,e,n){var r,i,o=e&&e.getIn(n),u=t&&t.getIn(n),s=e&&p(e,n),a=t&&p(t,n);return s&&!a?(i="delete",r=[o,n]):s&&a?(i="change",r=[u,o,n]):!s&&a&&(i="add",r=[u,n]),{eventName:i,args:r,newObject:u,oldObject:o}}function p(t,e){return t.hasIn?t.hasIn(e):t.getIn(e,L)!==L}function d(t,e){return function(n,r,i){var o=h(n,r,i);return o.eventName!==t&&"any"!==t?void 0:"any"===t?e.call(e,o.newObject,o.oldObject,i):e.apply(e,o.args)}}function v(t,e){return function(n,r,i){return e.call(this,n.getIn(t),r.getIn(t),i.slice(t.length))}}function y(t){return t&&"function"==typeof t.deref}function l(t){return D.some(function(e){return m(e.name,e.method,t)})}function m(t,e,n){return w[t]&&w[t][e]&&w[t][e](n)}function _(t){return"undefined"==typeof t?t:Array.isArray(t)?t:m("Iterable","isIterable",t)?t.toArray():[t]}function g(t,e){var n={};Object.getOwnPropertyNames(t.prototype).forEach(function(e){n[e]=Object.getOwnPropertyDescriptor(t.prototype,e)}),t.prototype=Object.create(e.prototype,n),t["super"]=e}var w="undefined"!=typeof window?window.Immutable:"undefined"!=typeof n?n.Immutable:null,b=t("immutable/contrib/cursor/index"),I=t("eventemitter3"),x=t("./utils"),k={};g(r,I),e.exports=r,r.prototype.cursor=function(t){var e=this;if(t=_(t)||[],!this.current)throw new Error("No structure loaded.");var n=function(t,n,r){return e.current===n?e.current=t:p(t,r)?e.current=e.current.setIn(r,t.getIn(r)):e.current=e.current.removeIn(r),e.current};return n=s(this,n),n=c(this,n),n=f(this,n),b.from(e.current,t,n)},r.prototype.reference=function(t){function e(){s=i.cursor(t)}function n(t,e){i._referencelisteners=o(i._referencelisteners,t,e)}function r(t,e){i._referencelisteners=u(i._referencelisteners,t,e)}y(t)&&t._keyPath&&(t=t._keyPath),t=_(t)||[];var i=this,s=this.cursor(t),a=w.Set();return n(t,e),{observe:function(e,i){return"function"==typeof e&&(i=e,e=void 0),this._dead||"function"!=typeof i?void 0:(i=e&&"swap"!==e?d(e,i):v(t,i),n(t,i),a=a.add(i),function(){r(t,i)})},cursor:function(t){return this._dead?void 0:(t=_(t),t?s.cursor(t):s)},reference:function(t){return t=_(t),i.reference((s._keyPath||[]).concat(t))},unobserveAll:function(n){return this._dead?void 0:(a.forEach(function(e){r(t,e)}),void(n&&r(t,e)))},destroy:function(){s=void 0,this.unobserveAll(!0),this._dead=!0,this.observe=void 0,this.unobserveAll=void 0,this.cursor=void 0,this.destroy=void 0,e=void 0,r=void 0,n=void 0}}},r.prototype.forceHasSwapped=function(t,e,n){this.emit("swap",t||this.current,e,n),a(this,t||this.current,e,n)},r.prototype.undo=function(t){return this._currentRevision-=t||1,this._currentRevision<0&&(this._currentRevision=0),this.current=this.history.get(this._currentRevision),this.current},r.prototype.redo=function(t){return this._currentRevision+=t||1,this._currentRevision>this.history.count()-1&&(this._currentRevision=this.history.count()-1),this.current=this.history.get(this._currentRevision),this.current},r.prototype.undoUntil=function(t){return this._currentRevision=this.history.indexOf(t),this.current=t,t};var O="undefined"!=typeof window&&window.requestAnimationFrame||x.raf,L={},D=[{name:"Iterable",method:"isIterable"},{name:"Seq",method:"isSeq"},{name:"Map",method:"isMap"},{name:"OrderedMap",method:"isOrderedMap"},{name:"List",method:"isList"},{name:"Stack",method:"isStack"},{name:"Set",method:"isSet"}]}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./utils":5,eventemitter3:2,"immutable/contrib/cursor/index":3}],5:[function(t,e,n){"use strict";e.exports.generateRandomKey=function(t){return t=t||10,Math.random().toString(36).substring(2).substring(0,t)},e.exports.raf=function(){for(var t="undefined"==typeof window?e:window,n=0,r=["webkit","moz"],i=0;i} instances Array of `Structure` instances. 16 | * 17 | * @class {Immstruct} 18 | * @constructor 19 | * @returns {Immstruct} 20 | * @api public 21 | */ 22 | function Immstruct () { 23 | if (!(this instanceof Immstruct)) { 24 | return new Immstruct(); 25 | } 26 | 27 | this._instances = {}; 28 | } 29 | 30 | /** 31 | * 32 | * Gets or creates a new instance of {Structure}. Provide optional 33 | * key to be able to retrieve it from list of instances. If no key 34 | * is provided, a random key will be generated. 35 | * 36 | * ### Examples: 37 | * ```js 38 | * var immstruct = require('immstruct'); 39 | * var structure = immstruct.get('myStruct', { foo: 'Hello' }); 40 | * ``` 41 | * @param {string} [key] - defaults to random string 42 | * @param {Object|Immutable} [data] - defaults to empty data 43 | * 44 | * @returns {Structure} 45 | * @module immstruct.get 46 | * @api public 47 | */ 48 | Immstruct.prototype.get = function (key, data) { 49 | return getInstance(this, { 50 | key: key, 51 | data: data 52 | }); 53 | }; 54 | 55 | /** 56 | * 57 | * Get list of all instances created. 58 | * 59 | * @param {string} [name] - Name of the instance to get. If undefined get all instances 60 | * 61 | * @returns {(Structure|Object.)} 62 | * @module immstruct.instance 63 | * @api public 64 | */ 65 | Immstruct.prototype.instance = function (name) { 66 | if (name) return this._instances[name]; 67 | return this._instances; 68 | }; 69 | 70 | /** 71 | * Clear the entire list of `Structure` instances from the Immstruct 72 | * instance. You would do this to start from scratch, freeing up memory. 73 | * 74 | * ### Examples: 75 | * ```js 76 | * var immstruct = require('immstruct'); 77 | * immstruct.clear(); 78 | * ``` 79 | * @module immstruct.clear 80 | * @api public 81 | */ 82 | Immstruct.prototype.clear = function () { 83 | this._instances = {}; 84 | }; 85 | 86 | /** 87 | * Remove one `Structure` instance from the Immstruct instances list. 88 | * Provided by key 89 | * 90 | * ### Examples: 91 | * ```js 92 | * var immstruct = require('immstruct'); 93 | * immstruct('myKey', { foo: 'hello' }); 94 | * immstruct.remove('myKey'); 95 | * ``` 96 | * @param {string} key 97 | * 98 | * @module immstruct.remove 99 | * @api public 100 | * @returns {boolean} 101 | */ 102 | Immstruct.prototype.remove = function (key) { 103 | return delete this._instances[key]; 104 | }; 105 | 106 | 107 | /** 108 | * Gets or creates a new instance of `Structure` with history (undo/redo) 109 | * activated per default. Same usage and signature as regular `Immstruct.get`. 110 | 111 | * Provide optional key to be able to retrieve it from list of instances. 112 | * If no key is provided, a random key will be generated. 113 | * 114 | * Provide optional limit to cap the last number of history references 115 | * that will be kept. Once limit is reached, a new history record 116 | * shifts off the oldest record. The default if omitted is Infinity. 117 | * Setting to 0 is the as not having history enabled in the first place. 118 | * 119 | * ### Examples: 120 | * ```js 121 | * var immstruct = require('immstruct'); 122 | * var structure = immstruct.withHistory('myStruct', 10, { foo: 'Hello' }); 123 | * var structure = immstruct.withHistory(10, { foo: 'Hello' }); 124 | * var structure = immstruct.withHistory('myStruct', { foo: 'Hello' }); 125 | * var structure = immstruct.withHistory({ foo: 'Hello' }); 126 | * ``` 127 | * 128 | * @param {string} [key] - defaults to random string 129 | * @param {number} [limit] - defaults to Infinity 130 | * @param {Object|Immutable} [data] - defaults to empty data 131 | * 132 | * @module immstruct.withHistory 133 | * @api public 134 | * @returns {Structure} 135 | */ 136 | Immstruct.prototype.withHistory = function (key, limit, data) { 137 | return getInstance(this, { 138 | key: key, 139 | data: data, 140 | history: true, 141 | historyLimit: limit 142 | }); 143 | }; 144 | 145 | var inst = new Immstruct(); 146 | 147 | /** 148 | * This is a default instance of `Immstruct` as well as a shortcut for 149 | * creating `Structure` instances (See `Immstruct.get` and `Immstruct`). 150 | * This is what is returned from `require('immstruct')`. 151 | * 152 | * From `Immstruct.get`: 153 | * Gets or creates a new instance of {Structure} in the default Immstruct 154 | * instance. A link to `immstruct.get()`. Provide optional 155 | * key to be able to retrieve it from list of instances. If no key 156 | * is provided, a random key will be generated. 157 | * 158 | * ### Examples: 159 | * ```js 160 | * var immstruct = require('immstruct'); 161 | * var structure = immstruct('myStruct', { foo: 'Hello' }); 162 | * var structure2 = immstruct.withHistory({ bar: 'Bye' }); 163 | * immstruct.remove('myStruct'); 164 | * // ... 165 | * ``` 166 | * 167 | * @param {string} [key] - defaults to random string 168 | * @param {Object|Immutable} [data] - defaults to empty data 169 | * 170 | * @api public 171 | * @see {@link Immstruct} 172 | * @see {Immstruct.prototype.get} 173 | * @module immstruct 174 | * @class {Immstruct} 175 | * @returns {Structure|Function} 176 | */ 177 | module.exports = function (key, data) { 178 | return getInstance(inst, { 179 | key: key, 180 | data: data 181 | }); 182 | }; 183 | 184 | module.exports.withHistory = function (key, limit, data) { 185 | return getInstance(inst, { 186 | key: key, 187 | data: data, 188 | history: true, 189 | historyLimit: limit 190 | }); 191 | }; 192 | 193 | module.exports.Structure = Structure; 194 | module.exports.Immstruct = Immstruct; 195 | module.exports.clear = inst.clear.bind(inst); 196 | module.exports.remove = inst.remove.bind(inst); 197 | module.exports.get = inst.get.bind(inst); 198 | module.exports.instance = function (name) { 199 | if (name) return inst._instances[name]; 200 | return inst._instances; 201 | }; 202 | 203 | function getInstance (obj, options) { 204 | if (typeof options.key === 'object') { 205 | options.data = options.key; 206 | options.key = void 0; 207 | } else if (typeof options.key === 'number') { 208 | options.data = options.historyLimit; 209 | options.historyLimit = options.key; 210 | options.key = void 0; 211 | } else if (typeof options.historyLimit === 'object') { 212 | options.data = options.historyLimit; 213 | options.historyLimit = void 0; 214 | } 215 | 216 | if (options.key && obj._instances[options.key]) { 217 | return obj._instances[options.key]; 218 | } 219 | 220 | var newInstance = new Structure(options); 221 | obj._instances[newInstance.key] = newInstance; 222 | return newInstance; 223 | } 224 | -------------------------------------------------------------------------------- /makeBundle.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var derequire = require('derequire'); 3 | var browserify = require('browserify'); 4 | var shim = require('browserify-shim'); 5 | 6 | var fs = require('fs'); 7 | var UglifyJS = require('uglify-js'); 8 | var pack = require('./package.json'); 9 | 10 | var inputFile = './index.js'; 11 | var outputFile = './dist/immstruct.js'; 12 | var outputMinifiedFile = './dist/immstruct.min.js'; 13 | 14 | var header = generateHeader(); 15 | 16 | var b = browserify({ 17 | standalone: 'immstruct' 18 | }); 19 | b.add(inputFile); 20 | b.exclude('immutable'); 21 | b.transform(shim); 22 | b.bundle(function(err, buf){ 23 | var code = buf.toString(); 24 | code = code.replace(/require\(('|")immutable('|")\)/ig, '(typeof window !== "undefined" ? window.Immutable : typeof global !== "undefined" ? global.Immutable : null)'); 25 | code = header + derequire(code); 26 | fs.writeFileSync(outputFile, code); 27 | 28 | var minfied = UglifyJS.minify(outputFile); 29 | fs.writeFileSync(outputMinifiedFile, header + minfied.code); 30 | }); 31 | 32 | function generateHeader() { 33 | var header = ''; 34 | 35 | header = '/**\n'; 36 | header += '* immstruct v' + pack.version + '\n'; 37 | header += '* A part of the Omniscient.js project\n'; 38 | header += '***************************************/\n'; 39 | 40 | return header; 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immstruct", 3 | "version": "2.0.0", 4 | "description": "Immutable data structure for top-to-bottom properties in component based libraries like React", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "mocha -R spec tests/*_test.js", 11 | "build-docs": "cat index.js src/structure.js | dox -r | doxme > api.md", 12 | "build-dist": "node ./makeBundle.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/omniscientjs/immstruct.git" 17 | }, 18 | "keywords": [ 19 | "immutable", 20 | "react", 21 | "structure", 22 | "properties" 23 | ], 24 | "author": "Mikael Brevik, @torgeir", 25 | "contributors": [ 26 | "Mikael Brevik", 27 | "@torgeir" 28 | ], 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/omniscientjs/immstruct/issues" 32 | }, 33 | "homepage": "https://github.com/omniscientjs/immstruct", 34 | "dependencies": { 35 | "eventemitter3": "^2.0.2", 36 | "immutable": "^3.1.0", 37 | "immutable-cursor": "^2.0.1" 38 | }, 39 | "devDependencies": { 40 | "browserify": "^14.1.0", 41 | "browserify-shim": "^3.8.9", 42 | "chai": "^3.5.0", 43 | "derequire": "^2.0.6", 44 | "dox": "^0.9.0", 45 | "doxme": "git://github.com/mikaelbr/doxme#dev", 46 | "mocha": "^3.2.0", 47 | "uglify-js": "^2.4.16" 48 | }, 49 | "browserify-shim": { 50 | "immutable": "global:Immutable" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/structure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Immutable = require('immutable'); 4 | var Cursor = require('immutable-cursor'); 5 | var EventEmitter = require('eventemitter3'); 6 | var utils = require('./utils'); 7 | 8 | var LISTENER_SENTINEL = {}; 9 | 10 | /** 11 | * Creates a new `Structure` instance. Also accessible through 12 | * `Immstruct.Structre`. 13 | * 14 | * A structure is also an EventEmitter object, so it has methods as 15 | * `.on`, `.off`, and all other EventEmitter methods. 16 | * 17 | * 18 | * For the `swap` event, the root structure (see `structure.current`) is passed 19 | * as arguments, but for type specific events (`add`, `change` and `delete`), the 20 | * actual changed value is passed. 21 | * 22 | * For instance: 23 | * ```js 24 | * var structure = new Structure({ 'data': { 'foo': { 'bar': 'hello' } } }); 25 | * 26 | * structure.on('swap', function (newData, oldData, keyPath) { 27 | * keyPath.should.eql(['foo', 'bar']); 28 | * newData.toJS().should.eql({ 'foo': { 'bar': 'bye' } }); 29 | * oldData.toJS().should.eql({ 'foo': { 'bar': 'hello' } }); 30 | * }); 31 | * 32 | * structure.cursor(['foo', 'bar']).update(function () { 33 | * return 'bye'; 34 | * }); 35 | * ``` 36 | * 37 | * But for `change` 38 | * ```js 39 | * var structure = new Structure({ 'data': { 'foo': { 'bar': 'hello' } } }); 40 | * 41 | * structure.on('change', function (newData, oldData, keyPath) { 42 | * keyPath.should.eql(['foo', 'bar']); 43 | * newData.should.eql('bye'); 44 | * oldData.should.eql('hello'); 45 | * }); 46 | * 47 | * structure.cursor(['foo', 'bar']).update(function () { 48 | * return 'bye'; 49 | * }); 50 | * ``` 51 | * 52 | * **All `keyPath`s passed to listeners are the full path to where the actual 53 | * change happened** 54 | * 55 | * ### Examples: 56 | * ```js 57 | * var Structure = require('immstruct/structure'); 58 | * var s = new Structure({ data: { foo: 'bar' }}); 59 | * 60 | * // Or: 61 | * // var Structure = require('immstruct').Structure; 62 | * ``` 63 | * 64 | * ### Events 65 | * 66 | * * `swap`: Emitted when cursor is updated (new information is set). Is emitted 67 | * on all types of changes, additions and deletions. The passed structures are 68 | * always the root structure. 69 | * One use case for this is to re-render design components. Callback 70 | * is passed arguments: `newStructure`, `oldStructure`, `keyPath`. 71 | * * `next-animation-frame`: Same as `swap`, but only emitted on animation frame. 72 | * Could use with many render updates and better performance. Callback is passed 73 | * arguments: `newStructure`, `oldStructure`, `keyPath`. 74 | * * `change`: Emitted when data/value is updated and it existed before. Emits 75 | * values: `newValue`, `oldValue` and `path`. 76 | * * `delete`: Emitted when data/value is removed. Emits value: `removedValue` and `path`. 77 | * * `add`: Emitted when new data/value is added. Emits value: `newValue` and `path`. 78 | * * `any`: With the same semantics as `add`, `change` or `delete`, `any` is triggered for 79 | * all types of changes. Differs from swap in the arguments that it is passed. 80 | * Is passed `newValue` (or undefined), `oldValue` (or undefined) and full `keyPath`. 81 | * New and old value are the changed value, not relative/scoped to the reference path as 82 | * with `swap`. 83 | * 84 | * ### Options 85 | * 86 | * ```json 87 | * { 88 | * key: string, // Defaults to random string 89 | * data: Object|Immutable, // defaults to empty Map 90 | * history: boolean, // Defaults to false 91 | * historyLimit: number, // If history enabled, Defaults to Infinity 92 | * } 93 | * ``` 94 | * 95 | * @property {Immutable.List} history `Immutable.List` with history. 96 | * @property {Object|Immutable} current Provided data as immutable data 97 | * @property {string} key Generated or provided key. 98 | * 99 | * 100 | * @param {{ key: string, data: Object, history: boolean }} [options] - defaults 101 | * to random key and empty data (immutable structure). No history 102 | * 103 | * @constructor 104 | * @class {Structure} 105 | * @returns {Structure} 106 | * @api public 107 | */ 108 | function Structure (options) { 109 | var self = this; 110 | 111 | options = options || {}; 112 | if (!(this instanceof Structure)) { 113 | return new Structure(options); 114 | } 115 | 116 | EventEmitter.call(this, arguments); 117 | 118 | this.key = options.key || utils.generateRandomKey(); 119 | 120 | this._queuedChange = false; 121 | this.current = options.data; 122 | if (!isImmutableStructure(this.current) || !this.current) { 123 | this.current = Immutable.fromJS(this.current || {}); 124 | } 125 | 126 | if (!!options.history) { 127 | this.history = Immutable.List.of(this.current); 128 | this._currentRevision = 0; 129 | this._historyLimit = (typeof options.historyLimit === 'number') ? 130 | options.historyLimit : 131 | Infinity; 132 | } 133 | 134 | this._referencelisteners = Immutable.Map(); 135 | this.on('swap', function (newData, oldData, keyPath) { 136 | keyPath = keyPath || []; 137 | var args = [newData, oldData, keyPath]; 138 | emit(self._referencelisteners, newData, oldData, keyPath, args); 139 | }); 140 | } 141 | inherits(Structure, EventEmitter); 142 | module.exports = Structure; 143 | 144 | function emit(map, newData, oldData, path, args) { 145 | if (!map || newData === oldData) return void 0; 146 | map.get(LISTENER_SENTINEL, []).forEach(function (fn) { 147 | fn.apply(null, args); 148 | }); 149 | 150 | if (path.length > 0) { 151 | var nextPathRoot = path[0]; 152 | var passedNewData = newData && newData.get ? newData.get(nextPathRoot) : void 0; 153 | var passedOldData = oldData && oldData.get ? oldData.get(nextPathRoot) : void 0; 154 | return emit(map.get(nextPathRoot), passedNewData, passedOldData, path.slice(1), args); 155 | } 156 | 157 | map.forEach(function(value, key) { 158 | if (key === LISTENER_SENTINEL) return void 0; 159 | var passedNewData = (newData && newData.get) ? newData.get(key) : void 0; 160 | var passedOldData = (oldData && oldData.get) ? oldData.get(key) : void 0; 161 | emit(value, passedNewData, passedOldData, [], args); 162 | }); 163 | } 164 | 165 | /** 166 | * Create a Immutable.js Cursor for a given `path` on the `current` structure (see `Structure.current`). 167 | * Changes made through created cursor will cause a `swap` event to happen (see `Events`). 168 | * 169 | * **This method returns a 170 | * [Immutable.js Cursor](https://github.com/facebook/immutable-js/blob/master/contrib/cursor/index.d.ts). 171 | * See the Immutable.js docs for more info on how to use cursors.** 172 | * 173 | * ### Examples: 174 | * ```js 175 | * var Structure = require('immstruct/structure'); 176 | * var s = new Structure({ data: { foo: 'bar', a: { b: 'foo' } }}); 177 | * s.cursor().set('foo', 'hello'); 178 | * s.cursor('foo').update(function () { return 'Changed'; }); 179 | * s.cursor(['a', 'b']).update(function () { return 'bar'; }); 180 | * ``` 181 | * 182 | * See more examples in the [tests](https://github.com/omniscientjs/immstruct/blob/master/tests/structure_test.js) 183 | * 184 | * @param {string|Array.} [path] - defaults to empty string. Can be array for path. See Immutable.js Cursors 185 | * 186 | * @api public 187 | * @module structure.cursor 188 | * @returns {Cursor} Gives a Cursor from Immutable.js 189 | */ 190 | Structure.prototype.cursor = function (path) { 191 | var self = this; 192 | path = valToKeyPath(path) || []; 193 | 194 | if (!this.current) { 195 | throw new Error('No structure loaded.'); 196 | } 197 | 198 | var changeListener = function (newRoot, oldRoot, path) { 199 | if(self.current === oldRoot) { 200 | self.current = newRoot; 201 | } else if(!hasIn(newRoot, path)) { 202 | // Othewise an out-of-sync change occured. We ignore `oldRoot`, and focus on 203 | // changes at path `path`, and sync this to `self.current`. 204 | self.current = self.current.removeIn(path); 205 | } else { 206 | // Update an existing path or add a new path within the current map. 207 | self.current = self.current.setIn(path, newRoot.getIn(path)); 208 | } 209 | 210 | return self.current; 211 | }; 212 | 213 | changeListener = handleHistory(this, changeListener); 214 | changeListener = handleSwap(this, changeListener); 215 | changeListener = handlePersisting(this, changeListener); 216 | return Cursor.from(self.current, path, changeListener); 217 | }; 218 | 219 | /** 220 | * Creates a reference. A reference can be a pointer to a cursor, allowing 221 | * you to create cursors for a specific path any time. This is essentially 222 | * a way to have "always updated cursors" or Reference Cursors. See example 223 | * for better understanding the concept. 224 | * 225 | * References also allow you to listen for changes specific for a path. 226 | * 227 | * ### Examples: 228 | * ```js 229 | * var structure = immstruct({ 230 | * someBox: { message: 'Hello World!' } 231 | * }); 232 | * var ref = structure.reference(['someBox']); 233 | * 234 | * var unobserve = ref.observe(function () { 235 | * // Called when data the path 'someBox' is changed. 236 | * // Also called when the data at ['someBox', 'message'] is changed. 237 | * }); 238 | * 239 | * // Update the data using the ref 240 | * ref.cursor().update(function () { return 'updated'; }); 241 | * 242 | * // Update the data using the initial structure 243 | * structure.cursor(['someBox', 'message']).update(function () { return 'updated again'; }); 244 | * 245 | * // Remove the listener 246 | * unobserve(); 247 | * ``` 248 | * 249 | * See more examples in the [readme](https://github.com/omniscientjs/immstruct) 250 | * 251 | * @param {string|Array.|Cursor} [path|cursor] - defaults to empty string. Can be 252 | * array for path or use path of cursor. See Immutable.js Cursors 253 | * 254 | * @api public 255 | * @module structure.reference 256 | * @returns {Reference} 257 | * @constructor 258 | */ 259 | Structure.prototype.reference = function reference (path) { 260 | if (isCursor(path) && path._keyPath) { 261 | path = path._keyPath; 262 | } 263 | 264 | path = valToKeyPath(path) || []; 265 | 266 | var self = this, 267 | cursor = this.cursor(path), 268 | unobservers = Immutable.Set(); 269 | 270 | function cursorRefresher() { cursor = self.cursor(path); } 271 | function _subscribe (path, fn) { 272 | self._referencelisteners = subscribe(self._referencelisteners, path, fn); 273 | } 274 | function _unsubscribe (path, fn) { 275 | self._referencelisteners = unsubscribe(self._referencelisteners, path, fn); 276 | } 277 | 278 | _subscribe(path, cursorRefresher); 279 | 280 | return { 281 | /** 282 | * Observe for changes on a reference. On references you can observe for changes, 283 | * but a reference **is not** an EventEmitter it self. 284 | * 285 | * The passed `keyPath` for swap events are relative to the reference, but 286 | * 287 | * 288 | * **Note**: As on `swap` for normal immstruct events, the passed arguments for 289 | * the event is the root, not guaranteed to be the actual changed value. 290 | * The structure is how ever scoped to the path passed in to the reference. 291 | * All values passed to the eventlistener for the swap event are relative 292 | * to the path used as key path to the reference. 293 | * 294 | * For instance: 295 | * 296 | * ```js 297 | * var structure = immstruct({ 'foo': { 'bar': 'hello' } }); 298 | * var ref = structure.reference('foo'); 299 | * ref.observe(function (newData, oldData, keyPath) { 300 | * keyPath.should.eql(['bar']); 301 | * newData.toJS().should.eql({ 'bar': 'updated' }); 302 | * oldData.toJS().should.eql({ 'bar': 'hello' }); 303 | * }); 304 | * ref.cursor().update(['bar'], function () { return 'updated'; }); 305 | * ``` 306 | * 307 | * For type specific events, how ever, the actual changed value is passed, 308 | * not the root data. In these cases, the full keyPath to the change is passed. 309 | * 310 | * For instance: 311 | * 312 | * ```js 313 | * var structure = immstruct({ 'foo': { 'bar': 'hello' } }); 314 | * var ref = structure.reference('foo'); 315 | * ref.observe('change', function (newValue, oldValue, keyPath) { 316 | * keyPath.should.eql(['foo', 'bar']); 317 | * newData.should.eql('updated'); 318 | * oldData.should.eql('hello'); 319 | * }); 320 | * ref.cursor().update(['bar'], function () { return 'updated'; }); 321 | * ``` 322 | * 323 | * 324 | * ### Examples: 325 | * ```js 326 | * var ref = structure.reference(['someBox']); 327 | * 328 | * var unobserve = ref.observe('delete', function () { 329 | * // Called when data the path 'someBox' is removed from the structure. 330 | * }); 331 | * ``` 332 | * 333 | * See more examples in the [readme](https://github.com/omniscientjs/immstruct) 334 | * 335 | * ### Events 336 | * * `swap`: Emitted when any cursor is updated (new information is set). 337 | * Triggered in any data swap is made on the structure. One use case for 338 | * this is to re-render design components. Data passed as arguments 339 | * are scoped/relative to the path passed to the reference, this also goes for keyPath. 340 | * Callback is passed arguments: `newStructure`, `oldStructure`, `keyPath`. 341 | * * `change`: Emitted when data/value is updated and it existed before. 342 | * Emits values: `newValue`, `oldValue` and `path`. 343 | * * `delete`: Emitted when data/value is removed. Emits value: `removedValue` and `path`. 344 | * * `add`: Emitted when new data/value is added. Emits value: `newValue` and `path`. 345 | * * `any`: With the same semantics as `add`, `change` or `delete`, `any` is triggered for 346 | * all types of changes. Differs from swap in the arguments that it is passed. 347 | * Is passed `newValue` (or undefined), `oldValue` (or undefined) and full `keyPath`. 348 | * New and old value are the changed value, not relative/scoped to the reference path as 349 | * with `swap`. 350 | * 351 | * @param {string} [eventName] - Type of change 352 | * @param {Function} callback - Callback when referenced data is swapped 353 | * 354 | * @api public 355 | * @module reference.observe 356 | * @returns {Function} Function for removing observer (unobserve) 357 | */ 358 | observe: function (eventName, newFn) { 359 | if (typeof eventName === 'function') { 360 | newFn = eventName; 361 | eventName = void 0; 362 | } 363 | if (this._dead || typeof newFn !== 'function') return; 364 | if (eventName && eventName !== 'swap') { 365 | newFn = onEventNameAndAny(eventName, newFn); 366 | } else { 367 | newFn = emitScopedReferencedStructures(path, newFn); 368 | } 369 | 370 | _subscribe(path, newFn); 371 | unobservers = unobservers.add(newFn); 372 | 373 | return function unobserve () { 374 | _unsubscribe(path, newFn); 375 | }; 376 | }, 377 | 378 | /** 379 | * Create a new, updated, cursor from the base path provded to the 380 | * reference. This returns a Immutable.js Cursor as the regular 381 | * cursor method. You can also provide a sub-path to create a reference 382 | * in a deeper level. 383 | * 384 | * ### Examples: 385 | * ```js 386 | * var ref = structure.reference(['someBox']); 387 | * var cursor = ref.cursor('someSubPath'); 388 | * var cursor2 = ref.cursor(); 389 | * ``` 390 | * 391 | * See more examples in the [readme](https://github.com/omniscientjs/immstruct) 392 | * 393 | * @param {string} [subpath] - Subpath to a deeper structure 394 | * 395 | * @api public 396 | * @module reference.cursor 397 | * @returns {Cursor} Immutable.js cursor 398 | */ 399 | cursor: function (subPath) { 400 | if (this._dead) return void 0; 401 | subPath = valToKeyPath(subPath); 402 | if (subPath) return cursor.cursor(subPath); 403 | return cursor; 404 | }, 405 | 406 | /** 407 | * Creates a reference on a lower level path. See creating normal references. 408 | * 409 | * ### Examples: 410 | * ```js 411 | * var structure = immstruct({ 412 | * someBox: { message: 'Hello World!' } 413 | * }); 414 | * var ref = structure.reference('someBox'); 415 | * 416 | * var newReference = ref.reference('message'); 417 | * ``` 418 | * 419 | * See more examples in the [readme](https://github.com/omniscientjs/immstruct) 420 | * 421 | * @param {string|Array.} [path] - defaults to empty string. Can be array for path. See Immutable.js Cursors 422 | * 423 | * @api public 424 | * @see structure.reference 425 | * @module reference.reference 426 | * @returns {Reference} 427 | */ 428 | reference: function (subPath) { 429 | subPath = valToKeyPath(subPath); 430 | return self.reference((cursor._keyPath || []).concat(subPath)); 431 | }, 432 | 433 | /** 434 | * Remove all observers from reference. 435 | * 436 | * @api public 437 | * @module reference.unobserveAll 438 | * @returns {Void} 439 | */ 440 | unobserveAll: function (destroy) { 441 | if (this._dead) return void 0; 442 | unobservers.forEach(function(fn) { 443 | _unsubscribe(path, fn); 444 | }); 445 | 446 | if (destroy) { 447 | _unsubscribe(path, cursorRefresher); 448 | } 449 | }, 450 | 451 | /** 452 | * Destroy reference. Unobserve all observers, set all endpoints of reference to dead. 453 | * For cleaning up memory. 454 | * 455 | * @api public 456 | * @module reference.destroy 457 | * @returns {Void} 458 | */ 459 | destroy: function () { 460 | cursor = void 0; 461 | this.unobserveAll(true); 462 | 463 | this._dead = true; 464 | this.observe = void 0; 465 | this.unobserveAll = void 0; 466 | this.cursor = void 0; 467 | this.destroy = void 0; 468 | 469 | cursorRefresher = void 0; 470 | _unsubscribe = void 0; 471 | _subscribe = void 0; 472 | } 473 | }; 474 | }; 475 | 476 | /** 477 | * Force emitting swap event. Pass on new, old and keypath passed to swap. 478 | * If newData is `null` current will be used. 479 | * 480 | * @param {Object} newData - Immutable object for the new data to emit 481 | * @param {Object} oldData - Immutable object for the old data to emit 482 | * @param {string} keyPath - Structure path (in tree) to where the changes occured. 483 | * 484 | * @api public 485 | * @module structure.forceHasSwapped 486 | * @returns {Void} 487 | */ 488 | Structure.prototype.forceHasSwapped = function (newData, oldData, keyPath) { 489 | this.emit('swap', newData || this.current, oldData, keyPath); 490 | possiblyEmitAnimationFrameEvent(this, newData || this.current, oldData, keyPath); 491 | }; 492 | 493 | 494 | /** 495 | * Undo IFF history is activated and there are steps to undo. Returns new current 496 | * immutable structure. 497 | * 498 | * **Will NOT emit swap when redo. You have to do this yourself**. 499 | * 500 | * Define number of steps to undo in param. 501 | * 502 | * @param {number} steps - Number of steps to undo 503 | * 504 | * @api public 505 | * @module structure.undo 506 | * @returns {Object} New Immutable structure after undo 507 | */ 508 | Structure.prototype.undo = function(steps) { 509 | this._currentRevision -= steps || 1; 510 | if (this._currentRevision < 0) { 511 | this._currentRevision = 0; 512 | } 513 | 514 | this.current = this.history.get(this._currentRevision); 515 | return this.current; 516 | }; 517 | 518 | /** 519 | * Redo IFF history is activated and you can redo. Returns new current immutable structure. 520 | * Define number of steps to redo in param. 521 | * **Will NOT emit swap when redo. You have to do this yourself**. 522 | * 523 | * @param {number} head - Number of steps to head to in redo 524 | * 525 | * @api public 526 | * @module structure.redo 527 | * @returns {Object} New Immutable structure after redo 528 | */ 529 | Structure.prototype.redo = function(head) { 530 | this._currentRevision += head || 1; 531 | if (this._currentRevision > this.history.count() - 1) { 532 | this._currentRevision = this.history.count() - 1; 533 | } 534 | 535 | this.current = this.history.get(this._currentRevision); 536 | return this.current; 537 | }; 538 | 539 | /** 540 | * Undo IFF history is activated and passed `structure` exists in history. 541 | * Returns the same immutable structure as passed as argument. 542 | * 543 | * **Will NOT emit swap after undo. You have to do this yourself**. 544 | * 545 | * @param {Object} structure - Immutable structure to redo until 546 | * 547 | * @api public 548 | * @module structure.undoUntil 549 | * @returns {Object} New Immutable structure after undo 550 | */ 551 | Structure.prototype.undoUntil = function(structure) { 552 | this._currentRevision = this.history.indexOf(structure); 553 | this.current = structure; 554 | 555 | return structure; 556 | }; 557 | 558 | 559 | function subscribe(listeners, path, fn) { 560 | return listeners.updateIn(path.concat(LISTENER_SENTINEL), Immutable.OrderedSet(), function(old) { 561 | return old.add(fn); 562 | }); 563 | } 564 | 565 | function unsubscribe(listeners, path, fn) { 566 | return listeners.updateIn(path.concat(LISTENER_SENTINEL), Immutable.OrderedSet(), function(old) { 567 | return old.remove(fn); 568 | }); 569 | } 570 | 571 | // Private decorators. 572 | 573 | // Update history if history is active 574 | function handleHistory (emitter, fn) { 575 | return function handleHistoryFunction (newData, oldData, path) { 576 | var newStructure = fn.apply(fn, arguments); 577 | if (!emitter.history || (newData === oldData)) return newStructure; 578 | 579 | emitter.history = emitter.history 580 | .take(++emitter._currentRevision) 581 | .push(emitter.current); 582 | 583 | if (emitter.history.size > emitter._historyLimit) { 584 | emitter.history = emitter.history.takeLast(emitter._historyLimit); 585 | emitter._currentRevision -= (emitter.history.size - emitter._historyLimit); 586 | } 587 | 588 | return newStructure; 589 | }; 590 | } 591 | 592 | var _requestAnimationFrame = (typeof window !== 'undefined' && 593 | window.requestAnimationFrame) || utils.raf; 594 | 595 | // Update history if history is active 596 | function possiblyEmitAnimationFrameEvent (emitter, newStructure, oldData, keyPath) { 597 | if (emitter._queuedChange) return void 0; 598 | emitter._queuedChange = true; 599 | 600 | _requestAnimationFrame(function () { 601 | emitter._queuedChange = false; 602 | emitter.emit('next-animation-frame', newStructure, oldData, keyPath); 603 | }); 604 | } 605 | 606 | // Emit swap event on values are swapped 607 | function handleSwap (emitter, fn) { 608 | return function handleSwapFunction (newData, oldData, keyPath) { 609 | var previous = emitter.current; 610 | var newStructure = fn.apply(fn, arguments); 611 | if(newData === previous) return newStructure; 612 | 613 | emitter.emit('swap', newStructure, previous, keyPath); 614 | possiblyEmitAnimationFrameEvent(emitter, newStructure, previous, keyPath); 615 | 616 | return newStructure; 617 | }; 618 | } 619 | 620 | // Map changes to update events (delete/change/add). 621 | function handlePersisting (emitter, fn) { 622 | return function handlePersistingFunction (newData, oldData, path) { 623 | var previous = emitter.current; 624 | var newStructure = fn.apply(fn, arguments); 625 | if(newData === previous) return newStructure; 626 | var info = analyze(newData, previous, path); 627 | 628 | if (info.eventName) { 629 | emitter.emit.apply(emitter, [info.eventName].concat(info.args)); 630 | emitter.emit('any', info.newObject, info.oldObject, path); 631 | } 632 | return newStructure; 633 | }; 634 | } 635 | 636 | // Private helpers. 637 | 638 | function analyze (newData, oldData, path) { 639 | var oldObject = oldData && oldData.getIn(path); 640 | var newObject = newData && newData.getIn(path); 641 | 642 | var inOld = oldData && hasIn(oldData, path); 643 | var inNew = newData && hasIn(newData, path); 644 | 645 | var args, eventName; 646 | 647 | if (inOld && !inNew) { 648 | eventName = 'delete'; 649 | args = [oldObject, path]; 650 | } else if (inOld && inNew) { 651 | eventName = 'change'; 652 | args = [newObject, oldObject, path]; 653 | } else if (!inOld && inNew) { 654 | eventName = 'add'; 655 | args = [newObject, path]; 656 | } 657 | 658 | return { 659 | eventName: eventName, 660 | args: args, 661 | newObject: newObject, 662 | oldObject: oldObject 663 | }; 664 | } 665 | 666 | // Check if path exists. 667 | var NOT_SET = {}; 668 | function hasIn(cursor, path) { 669 | if(cursor.hasIn) return cursor.hasIn(path); 670 | return cursor.getIn(path, NOT_SET) !== NOT_SET; 671 | } 672 | 673 | function onEventNameAndAny(eventName, fn) { 674 | return function (newData, oldData, keyPath) { 675 | var info = analyze(newData, oldData, keyPath); 676 | 677 | if (info.eventName !== eventName && eventName !== 'any') return void 0; 678 | if (eventName === 'any') { 679 | return fn.call(fn, info.newObject, info.oldObject, keyPath); 680 | } 681 | return fn.apply(fn, info.args); 682 | }; 683 | } 684 | 685 | function emitScopedReferencedStructures(path, fn) { 686 | return function withReferenceScopedStructures (newStructure, oldStructure, keyPath) { 687 | return fn.call(this, newStructure.getIn(path), oldStructure.getIn(path), keyPath.slice(path.length)); 688 | }; 689 | } 690 | 691 | function isCursor (potential) { 692 | return potential && typeof potential.deref === 'function'; 693 | } 694 | 695 | // Check if passed structure is existing immutable structure. 696 | // From https://github.com/facebook/immutable-js/wiki/Upgrading-to-Immutable-v3#additional-changes 697 | var immutableCheckers = [ 698 | {name: 'Iterable', method: 'isIterable' }, 699 | {name: 'Seq', method: 'isSeq'}, 700 | {name: 'Map', method: 'isMap'}, 701 | {name: 'OrderedMap', method: 'isOrderedMap'}, 702 | {name: 'List', method: 'isList'}, 703 | {name: 'Stack', method: 'isStack'}, 704 | {name: 'Set', method: 'isSet'} 705 | ]; 706 | function isImmutableStructure (data) { 707 | return immutableCheckers.some(function (checkItem) { 708 | return immutableSafeCheck(checkItem.name, checkItem.method, data); 709 | }); 710 | } 711 | 712 | function immutableSafeCheck (ns, method, data) { 713 | return Immutable[ns] && Immutable[ns][method] && Immutable[ns][method](data); 714 | } 715 | 716 | function valToKeyPath(val) { 717 | if (typeof val === 'undefined') { 718 | return val; 719 | } 720 | return Array.isArray(val) ? val : 721 | immutableSafeCheck('Iterable', 'isIterable', val) ? 722 | val.toArray() : [val]; 723 | } 724 | 725 | function inherits (c, p) { 726 | var e = {}; 727 | Object.getOwnPropertyNames(c.prototype).forEach(function (k) { 728 | e[k] = Object.getOwnPropertyDescriptor(c.prototype, k); 729 | }); 730 | c.prototype = Object.create(p.prototype, e); 731 | c['super'] = p; 732 | } 733 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateRandomKey = function (len) { 4 | len = len || 10; 5 | return Math.random().toString(36).substring(2).substring(0, len); 6 | }; 7 | 8 | // Variation shim based on the classic polyfill: 9 | // http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ 10 | module.exports.raf = (function() { 11 | var glob = (typeof window === 'undefined') ? module : window; 12 | var lastTime = 0; 13 | var vendors = ['webkit', 'moz']; 14 | for(var x = 0; x < vendors.length && !glob.requestAnimationFrame; ++x) { 15 | glob.requestAnimationFrame = glob[vendors[x]+'RequestAnimationFrame']; 16 | } 17 | 18 | return function(callback, element) { 19 | var currTime = new Date().getTime(); 20 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 21 | var id = setTimeout(function() { callback(currTime + timeToCall); }, 22 | timeToCall); 23 | lastTime = currTime + timeToCall; 24 | return id; 25 | }; 26 | }()); 27 | -------------------------------------------------------------------------------- /structure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = require('./src/structure'); -------------------------------------------------------------------------------- /tests/immstruct_test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | chai.should(); 3 | 4 | var immstruct = require('../'); 5 | 6 | var EventEmitter = require('eventemitter3'); 7 | 8 | describe('immstruct', function () { 9 | 10 | afterEach(function () { 11 | immstruct.clear(); 12 | }); 13 | 14 | it('should return an EventEmitter3', function () { 15 | immstruct('customKey').should.be.instanceof(EventEmitter); 16 | }); 17 | 18 | it('should expose Structure', function () { 19 | immstruct.should.have.property('Structure'); 20 | }); 21 | 22 | it('should expose instancible Immstruct', function () { 23 | immstruct.should.have.property('Immstruct'); 24 | }); 25 | 26 | it('should support creating a new immstruct structure without new (to be safe)', function () { 27 | (immstruct.Immstruct()).should.be.instanceof(immstruct.Immstruct); 28 | }); 29 | 30 | it('should create empty immutable structure and random key if no input', function () { 31 | var structure = immstruct(); 32 | structure.current.toJS().should.be.an('object'); 33 | structure.key.should.be.an('string'); 34 | }); 35 | 36 | it('should create empty immutable structure and random key if no input on local instance', function () { 37 | var local = new immstruct.Immstruct(); 38 | var structure = local.get(); 39 | structure.current.toJS().should.be.an('object'); 40 | structure.key.should.be.an('string'); 41 | }); 42 | 43 | it('local instance and global instance should not share instances', function () { 44 | var local = new immstruct.Immstruct(); 45 | var structure = local.get(); 46 | var stdStructure = immstruct(); 47 | 48 | local.instance(structure.key).should.not.equal(stdStructure); 49 | 50 | local.instance().should.be.an('object'); 51 | stdStructure.should.be.an('object'); 52 | structure.should.be.an('object'); 53 | }); 54 | 55 | it('should be able to create structure with history', function () { 56 | var structure = immstruct.withHistory({ foo: 'bar' }); 57 | structure.current.toJS().should.be.an('object'); 58 | structure.history.get(0).toJS().should.eql({ foo: 'bar' }); 59 | }); 60 | 61 | it('should be able to create structure with history and key', function () { 62 | var structure = immstruct.withHistory('histAndKey', { foo: 'bar' }); 63 | structure.current.toJS().should.be.an('object'); 64 | structure.history.get(0).toJS().should.eql({ foo: 'bar' }); 65 | var structureRef = immstruct('histAndKey'); 66 | structureRef.history.get(0).toJS().should.eql({ foo: 'bar' }); 67 | }); 68 | 69 | it('should be able to create structure with history and key on instance', function () { 70 | var localImmstruct = new immstruct.Immstruct(); 71 | var structure = localImmstruct.withHistory('histAndKey', { foo: 'bar' }); 72 | structure.current.toJS().should.be.an('object'); 73 | structure.history.get(0).toJS().should.eql({ foo: 'bar' }); 74 | var structureRef = localImmstruct.instance('histAndKey'); 75 | structureRef.history.get(0).toJS().should.eql({ foo: 'bar' }); 76 | }); 77 | 78 | it('should be able to create structure with capped history', function () { 79 | var limit = 1; // only keep this many history refs 80 | var structure = immstruct.withHistory(limit, { foo: 'bar' }); 81 | structure.current.toJS().should.be.an('object'); 82 | structure.history.get(0).toJS().should.eql({ foo: 'bar' }); 83 | structure.cursor('foo').update(function () { return 'cat'; }); 84 | structure.history.size.should.eql(1); 85 | structure.history.get(0).toJS().should.eql({ foo: 'cat' }); 86 | }); 87 | 88 | it('should be able to create structure with capped history and key', function () { 89 | var limit = 1; // only keep this many history refs 90 | var structure = immstruct.withHistory('histLimitKey', limit, { foo: 'bar' }); 91 | structure.current.toJS().should.be.an('object'); 92 | structure.history.get(0).toJS().should.eql({ foo: 'bar' }); 93 | structure.cursor('foo').update(function () { return 'cat'; }); 94 | structure.history.size.should.eql(1); 95 | structure.history.get(0).toJS().should.eql({ foo: 'cat' }); 96 | var structureRef = immstruct('histLimitKey'); 97 | structureRef.history.get(0).toJS().should.eql({ foo: 'cat' }); 98 | }); 99 | 100 | it('should give structure with random key when js object given as only argument', function () { 101 | var structure = immstruct({ foo: 'hello' }); 102 | structure.current.toJS().should.have.property('foo'); 103 | structure.key.should.be.an('string'); 104 | }); 105 | 106 | it('should give structure with key and default object if only key is given and structure dont exist', function () { 107 | var structure = immstruct('customKey'); 108 | structure.current.toJS().should.be.an('object'); 109 | structure.key.should.equal('customKey'); 110 | }); 111 | 112 | it('should give structure with key and default object if only key is given and structure dont exist', function () { 113 | var structure = immstruct('customKey2'); 114 | structure.current.toJS().should.be.an('object'); 115 | structure.key.should.not.equal('customKey'); 116 | }); 117 | 118 | it('should be able to retrieve created structure with same key', function () { 119 | immstruct('customKey', { foo: 'hello' }); 120 | immstruct('customKey').current.toJS().should.have.property('foo'); 121 | immstruct('customKey').current.toJS().foo.should.equal('hello'); 122 | }); 123 | 124 | it('should clear all instances', function () { 125 | immstruct('customKey', { 'foo': 'hello' }); 126 | immstruct('customKey').current.toJS().should.have.property('foo'); 127 | immstruct.clear(); 128 | immstruct('customKey').current.toJS().should.not.have.property('foo'); 129 | }); 130 | 131 | it('should delete structure', function () { 132 | immstruct('customKey', { 'foo': 'hello' }); 133 | immstruct('customKey').current.toJS().should.have.property('foo'); 134 | immstruct.remove('customKey').should.equal(true); 135 | immstruct('customKey').current.toJS().should.not.have.property('foo'); 136 | }); 137 | 138 | it('should expose the instances internals', function () { 139 | immstruct('customKey', { 'foo': 'hello' }); 140 | immstruct.instance('customKey').current.toJS().should.have.property('foo'); 141 | }); 142 | 143 | it('should expose all instances', function () { 144 | immstruct('customKey', { 'foo': 'hello' }); 145 | immstruct.instance().customKey.current.toJS().should.have.property('foo'); 146 | }); 147 | 148 | }) 149 | -------------------------------------------------------------------------------- /tests/structure_test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | chai.should(); 4 | 5 | var Immutable = require('immutable'); 6 | var Structure = require('../src/structure'); 7 | 8 | describe('structure', function () { 9 | 10 | 11 | describe('api', function () { 12 | it('should be new safe to avoid mishaps of forgetting new keyword', function () { 13 | var structure = Structure({ 14 | data: { 'foo': 'hello' } 15 | }); 16 | 17 | structure.should.be.instanceof(Structure); 18 | }); 19 | 20 | it('should expose immutable.js cursor', function () { 21 | var structure = new Structure({ 22 | data: { 'foo': 'hello' } 23 | }); 24 | 25 | var cursor = structure.cursor(['foo']); 26 | cursor.deref().should.equal('hello'); 27 | cursor = cursor.update(function () { 28 | return 'bar'; 29 | }); 30 | cursor.deref().should.equal('bar'); 31 | }); 32 | 33 | 34 | it('should allow values as key paths', function () { 35 | var s = new Structure({ data: {'': 3, a: {'': 5}} }); 36 | s.cursor().cursor('').deref().should.equal(3); 37 | s.cursor('').deref().should.equal(3); 38 | 39 | var r = s.reference('a'); 40 | r.cursor().cursor('').deref().should.equal(5); 41 | r.cursor('').deref().should.equal(5); 42 | }); 43 | 44 | }); 45 | 46 | describe('existing immutable structure', function () { 47 | 48 | it('should accept existing immutable maps', function () { 49 | var immutableObj = Immutable.fromJS({ 50 | foo: 'hello' 51 | }); 52 | 53 | var structure = new Structure({ 54 | data: immutableObj 55 | }); 56 | 57 | var cursor = structure.cursor(['foo']); 58 | cursor.deref().should.equal('hello'); 59 | cursor = cursor.update(function () { 60 | return 'bar'; 61 | }); 62 | cursor.deref().should.equal('bar'); 63 | }); 64 | 65 | it('should accept existing immutable list', function () { 66 | var immutableObj = Immutable.List.of('hello'); 67 | 68 | var structure = new Structure({ 69 | data: immutableObj 70 | }); 71 | 72 | var cursor = structure.cursor(['0']); 73 | 74 | cursor.deref().should.equal('hello'); 75 | cursor = cursor.update(function () { 76 | return 'bar'; 77 | }); 78 | cursor.deref().should.equal('bar'); 79 | }); 80 | 81 | }); 82 | 83 | describe('events', function () { 84 | it('should trigger swap when structure is changed with new and old data', function (done) { 85 | var structure = new Structure({ 86 | data: { 'foo': 'hello' } 87 | }); 88 | structure.on('swap', function (newData, oldData, keyPath) { 89 | keyPath.should.eql(['foo']); 90 | newData.toJS().should.eql({'foo': 'bar'}); 91 | oldData.toJS().should.eql({'foo': 'hello'}); 92 | structure.cursor().toJS().should.eql({ 'foo': 'bar' }); 93 | done(); 94 | }); 95 | 96 | structure.cursor(['foo']).update(function () { 97 | return 'bar'; 98 | }); 99 | }); 100 | 101 | it('should trigger swap when structure is changed with new and old data on nested cursors', function (done) { 102 | var structure = new Structure({ 103 | data: { 'foo': { 'bar': 'hello' } } 104 | }); 105 | structure.on('swap', function (newData, oldData, keyPath) { 106 | keyPath.should.eql(['foo', 'bar']); 107 | newData.toJS().should.eql({ 'foo': { 'bar': 'bye' } }); 108 | oldData.toJS().should.eql({ 'foo': { 'bar': 'hello' } }); 109 | done(); 110 | }); 111 | 112 | structure.cursor(['foo']).cursor(['bar']).update(function () { 113 | return 'bye'; 114 | }); 115 | }); 116 | 117 | it('should trigger swap with keyPath', function (done) { 118 | var structure = new Structure({ 119 | data: { 'foo': 'hello' } 120 | }); 121 | 122 | structure.on('swap', function (newData, oldData, keyPath) { 123 | keyPath.should.eql(['foo']); 124 | done(); 125 | }); 126 | 127 | structure.cursor(['foo']).update(function () { 128 | return 'bar'; 129 | }); 130 | }); 131 | 132 | it('should trigger swap with keyPath on forceHasSwapped', function (done) { 133 | var structure = new Structure({ 134 | data: { 'foo': 'hello' } 135 | }); 136 | 137 | structure.on('swap', function (newData, oldData, keyPath) { 138 | keyPath.should.eql(['foo']); 139 | done(); 140 | }); 141 | 142 | structure.forceHasSwapped(structure.current, structure.current, ['foo']) 143 | }); 144 | 145 | it('should not emit events nor affect history when updating the structure does not actually change anything', function () { 146 | 147 | var calls = 0; 148 | var structure = new Structure({ 149 | data: { foo: {}, bar: 42 }, 150 | history: true 151 | }); 152 | var original = structure.current; 153 | 154 | structure.on('swap', function() { 155 | calls++; 156 | }); 157 | 158 | structure.on('change', function() { 159 | calls++; 160 | }); 161 | 162 | var cursor = structure.cursor(); 163 | var cursorBar = structure.cursor('bar'); 164 | 165 | cursor.set('foo', Immutable.Map()); 166 | 167 | calls.should.equal(0); 168 | 169 | cursorBar.update(function() { 170 | return 42; 171 | }); 172 | 173 | calls.should.equal(0); 174 | original.should.equal(structure.current); 175 | 176 | // Ensure history is not affected 177 | structure.history.size.should.equal(1); 178 | 179 | }); 180 | 181 | it('should trigger any event on any type of add, delete and change', function (done) { 182 | var structure = new Structure({ 183 | data: { } 184 | }); 185 | var numberOfCalls = 0; 186 | 187 | structure.on('any', function (newData, oldData, keyPath) { 188 | numberOfCalls++; 189 | 190 | if (numberOfCalls === 6) { 191 | done(); 192 | } 193 | }); 194 | 195 | structure.on('add', function (newData, keyPath) { 196 | numberOfCalls++; 197 | keyPath.should.eql(['foo']); 198 | newData.should.eql('baz'); 199 | }); 200 | structure.cursor(['foo']).update(function (state) { 201 | return 'baz'; 202 | }); 203 | 204 | structure.on('change', function (newData, oldData, keyPath) { 205 | numberOfCalls++; 206 | keyPath.should.eql(['foo']); 207 | newData.should.equal('updated'); 208 | oldData.should.equal('baz'); 209 | }); 210 | structure.cursor('foo').update(function () { return 'updated'; }); 211 | 212 | structure.on('delete', function (oldData, keyPath) { 213 | numberOfCalls++; 214 | keyPath.should.eql(['foo']); 215 | oldData.should.eql('updated'); 216 | 217 | if (numberOfCalls === 6) { 218 | done(); 219 | } 220 | }); 221 | structure.cursor().remove('foo'); 222 | }); 223 | 224 | it('should be able to force trigger swap', function (done) { 225 | var structure = new Structure(); 226 | structure.on('swap', function () { 227 | done(); 228 | }); 229 | structure.forceHasSwapped(); 230 | }); 231 | 232 | it('should trigger change with data when existing property is changed', function (done) { 233 | var structure = new Structure({ 234 | data: { 'foo': 'hello' } 235 | }); 236 | 237 | structure.on('change', function (newValue, oldValue, path) { 238 | path.should.eql(['foo']); 239 | oldValue.should.equal('hello'); 240 | newValue.should.equal('bar'); 241 | structure.current.toJS().should.eql({ 242 | foo: 'bar' 243 | }); 244 | done(); 245 | }); 246 | 247 | structure.cursor(['foo']).update(function () { 248 | return 'bar'; 249 | }); 250 | }); 251 | 252 | it('should trigger change with data when existing property is changed to falsey value', function (done) { 253 | var structure = new Structure({ 254 | data: { 'foo': true } 255 | }); 256 | var i = 0; 257 | structure.on('change', function (newValue, oldValue, path) { 258 | path.should.eql(['foo']); 259 | switch(i) { 260 | case 0: 261 | oldValue.should.equal(true); 262 | expect(newValue).to.be.undefined; 263 | break; 264 | case 1: 265 | expect(oldValue).to.be.undefined; 266 | expect(newValue).to.be.false; 267 | break; 268 | case 2: 269 | expect(oldValue).to.be.false; 270 | expect(newValue).to.be.null; 271 | done(); 272 | break; 273 | } 274 | i++; 275 | }); 276 | 277 | structure.cursor(['foo']).update(function () { 278 | return void 0; 279 | }); 280 | 281 | structure.cursor(['foo']).update(function () { 282 | return false; 283 | }); 284 | 285 | structure.cursor(['foo']).update(function () { 286 | return null; 287 | }); 288 | }); 289 | 290 | 291 | it('should trigger add with data when a new property is added', function (done) { 292 | var structure = new Structure({ 293 | data: { 'foo': 'hello' } 294 | }); 295 | 296 | structure.on('add', function (newValue, path) { 297 | path.should.eql(['bar']); 298 | newValue.should.equal('baz'); 299 | structure.current.toJS().should.eql({ 300 | foo: 'hello', 301 | bar: 'baz' 302 | }); 303 | done(); 304 | }); 305 | 306 | structure.cursor(['bar']).update(function (state) { 307 | return 'baz'; 308 | }); 309 | }); 310 | 311 | 312 | it('should trigger add with data when a new property added is a falsey value', function (done) { 313 | var structure = new Structure({ 314 | data: { 'foo': 'hello' } 315 | }); 316 | var i = 1; 317 | structure.on('add', function (newValue, path) { 318 | path.should.eql([i+'']); 319 | switch(i) { 320 | case 1: 321 | expect(newValue).to.be.false; 322 | break; 323 | case 2: 324 | expect(newValue).to.be.null; 325 | break; 326 | case 3: 327 | expect(newValue).to.be.undefined; 328 | done(); 329 | break; 330 | } 331 | i++; 332 | }); 333 | 334 | structure.cursor(['1']).update(function () { 335 | return false; 336 | }); 337 | 338 | structure.cursor(['2']).update(function () { 339 | return null; 340 | }); 341 | 342 | structure.cursor().set('3', void 0); 343 | }); 344 | 345 | 346 | it('should trigger delete with data when existing property is removed', function (done) { 347 | var structure = new Structure({ 348 | data: { 'foo': 'hello', 'bar': 'world' } 349 | }); 350 | 351 | structure.on('delete', function (oldValue, path) { 352 | path.should.eql(['foo']); 353 | oldValue.should.equal('hello'); 354 | structure.cursor().toJS().should.eql({ 'bar': 'world' }); 355 | done(); 356 | }); 357 | 358 | structure.cursor().remove('foo'); 359 | }); 360 | 361 | describe('concurrency', function () { 362 | it('should set correct structure when modifying it during a swap event', function (done) { 363 | var structure = new Structure({ 364 | data: { 'foo': 'hello' } 365 | }); 366 | var i = 0; 367 | structure.on('swap', function (newData, oldData) { 368 | i++; 369 | if(i == 1) { 370 | newData.toJS().should.eql({ 'foo': 'bar' }); 371 | oldData.toJS().should.eql({ 'foo': 'hello' }); 372 | structure.cursor().toJS().should.eql({ 'foo': 'bar' }); 373 | } 374 | if(i == 2) { 375 | newData.toJS().should.eql({ 'foo': 'bar', 'bar': 'world' }); 376 | oldData.toJS().should.eql({ 'foo': 'bar' }); 377 | structure.cursor().toJS().should.eql({'foo': 'bar', 'bar': 'world'}); 378 | done(); 379 | } 380 | }); 381 | structure.once('swap', function (newData, oldData) { 382 | structure.cursor('bar').update(function() { 383 | return 'world'; 384 | }); 385 | }); 386 | structure.cursor(['foo']).update(function () { 387 | return 'bar'; 388 | }); 389 | }); 390 | 391 | it('should set correct structure when modifying it during a change event', function (done) { 392 | var structure = new Structure({ 393 | data: { 'subtree': {} } 394 | }); 395 | var i = 0; 396 | structure.on('swap', function (newData, oldData) { 397 | i++; 398 | if(i == 1) { 399 | newData.toJS().should.eql({ subtree: { foo: 'bar' } }); 400 | oldData.toJS().should.eql({ subtree: {} }); 401 | } 402 | if(i == 2) { 403 | newData.toJS().should.eql({ subtree: { foo: 'bar', hello: 'world' } }); 404 | oldData.toJS().should.eql({ subtree: { foo: 'bar' } }); 405 | structure.cursor().toJS().should.eql({ subtree: { foo: 'bar', hello: 'world' } }); 406 | done(); 407 | } 408 | }); 409 | structure.once('change', function (newValue, oldValue, path) { 410 | path.should.eql(['subtree']); 411 | newValue.toJS().should.eql({ foo: 'bar' }); 412 | oldValue.toJS().should.eql({}); 413 | structure.cursor('subtree').update('hello',function() { 414 | return 'world'; 415 | }); 416 | }); 417 | structure.cursor().update('subtree', function () { 418 | return Immutable.fromJS({ 419 | foo: 'bar' 420 | }); 421 | }); 422 | }); 423 | 424 | it('should avoid stale cursor changed existing property', function() { 425 | 426 | var struct = new Structure({ 427 | data: { foo: 42, bar: 24 } 428 | }); 429 | 430 | var foo = struct.cursor('foo'); 431 | var bar = struct.cursor('bar'); 432 | var current = struct.current; 433 | 434 | struct.once('swap', function(newRoot, oldRoot) { 435 | oldRoot.toJS().should.eql({ foo: 42, bar: 24 }); 436 | current.should.equal(oldRoot); 437 | foo._rootData.should.equal(current); 438 | 439 | newRoot.toJS().should.eql({ foo: 43, bar: 24 }); 440 | struct.current.toJS().should.eql(newRoot.toJS()); 441 | }); 442 | 443 | foo.update(function(val) { 444 | val.should.equal(42); 445 | return val + 1; 446 | }); 447 | 448 | struct.current.toJS().should.eql({ foo: 43, bar: 24 }); 449 | 450 | current = struct.current; 451 | struct.once('swap', function(newRoot, oldRoot) { 452 | oldRoot.toJS().should.eql({ foo: 43, bar: 24 }); 453 | current.toJS().should.not.eql(newRoot.toJS()); 454 | newRoot.toJS().should.not.eql(oldRoot.toJS()); 455 | 456 | newRoot.toJS().should.eql({ foo: 43, bar: 25 }); 457 | }); 458 | 459 | bar.update(function(val) { 460 | val.should.equal(24); 461 | return val + 1; 462 | }); 463 | 464 | struct.current.toJS().should.eql({ foo: 43, bar: 25 }); 465 | }); 466 | 467 | it('should set correct structure when modifying it during an add event', function (done) { 468 | var structure = new Structure({ 469 | data: { } 470 | }); 471 | var i = 0; 472 | structure.on('swap', function (newData, oldData) { 473 | i++; 474 | if(i == 1) { 475 | newData.toJS().should.eql({ subtree: { foo: 'bar' } }); 476 | oldData.toJS().should.eql({}); 477 | structure.cursor().toJS().should.eql({ subtree: { foo: 'bar' } }); 478 | } 479 | if(i == 2) { 480 | structure.cursor().toJS().should.eql({ subtree: { foo: 'bar', hello: 'world' } }); 481 | done(); 482 | } 483 | }); 484 | structure.once('add', function (newValue, path) { 485 | path.should.eql(['subtree']); 486 | newValue.toJS().should.eql({ foo: 'bar' }); 487 | structure.cursor('subtree').update('hello',function() { 488 | return 'world'; 489 | }); 490 | }); 491 | structure.cursor().update('subtree', function () { 492 | return Immutable.fromJS({ 493 | foo: 'bar' 494 | }); 495 | }); 496 | }); 497 | 498 | it('should avoid stale cursor when adding a new property', function() { 499 | 500 | var struct = new Structure({ 501 | data: { foo: {}, bar: {} } 502 | }); 503 | 504 | var foo = struct.cursor('foo'); 505 | var bar = struct.cursor('bar'); 506 | var current = struct.current; 507 | 508 | struct.once('swap', function(newRoot, oldRoot) { 509 | oldRoot.toJS().should.eql({ foo: {}, bar: {} }); 510 | current.should.equal(oldRoot); 511 | foo._rootData.should.equal(current); 512 | 513 | newRoot.toJS().should.eql({ foo: { a: 42 }, bar: {} }); 514 | struct.current.toJS().should.eql(newRoot.toJS()); 515 | }); 516 | 517 | foo.update('a', function() { 518 | return 42; 519 | }); 520 | 521 | struct.current.toJS().should.eql({ foo: {a: 42}, bar: {} }); 522 | 523 | current = struct.current; 524 | struct.once('swap', function(newRoot, oldRoot) { 525 | oldRoot.toJS().should.eql({ foo: {a: 42}, bar: {} }); 526 | 527 | current.toJS().should.eql(oldRoot.toJS()); 528 | current.toJS().should.not.eql(newRoot.toJS()); 529 | newRoot.toJS().should.not.eql(oldRoot.toJS()); 530 | 531 | newRoot.toJS().should.eql({ foo: {a: 42}, bar: { b: "hello" } }); 532 | }); 533 | // This test case demonstrates the distinction between 534 | // .setIn(path, newRoot.getIn(path)) and 535 | // .updateIn(path, () => newRoot.getIn(path)) 536 | bar.set('b', "hello"); 537 | 538 | struct.current.toJS().should.eql({ foo: {a: 42}, bar: { b: "hello" } }); 539 | }); 540 | 541 | it('should set correct structure when modifying it during a delete event', function (done) { 542 | var structure = new Structure({ 543 | data: { 'subtree': {} } 544 | }); 545 | var i = 0; 546 | structure.on('swap', function (newData, oldData) { 547 | i++; 548 | if(i == 1) { 549 | newData.toJS().should.eql({}); 550 | oldData.toJS().should.eql({ subtree: {} }); 551 | } 552 | if(i == 2) { 553 | newData.toJS().should.eql({ subtree: { hello: 'world'} }); 554 | oldData.toJS().should.eql({}); 555 | structure.cursor().toJS().should.eql({ subtree: { hello: 'world' } }); 556 | done(); 557 | } 558 | }); 559 | structure.once('delete', function (newValue, path) { 560 | path.should.eql(['subtree']); 561 | newValue.toJS().should.eql({}); 562 | structure.cursor('subtree').update('hello',function() { 563 | return 'world'; 564 | }); 565 | }); 566 | structure.cursor().delete('subtree'); 567 | }); 568 | 569 | it('should avoid stale cursor when deleting existing property', function() { 570 | 571 | var struct = new Structure({ 572 | data: { foo: { a: 42 }, bar: { b: 24 } } 573 | }); 574 | 575 | var foo = struct.cursor('foo'); 576 | var bar = struct.cursor('bar'); 577 | var current = struct.current; 578 | 579 | struct.once('swap', function(newRoot, oldRoot) { 580 | oldRoot.toJS().should.eql({ foo: { a: 42 }, bar: { b: 24 } }); 581 | current.should.equal(oldRoot); 582 | foo._rootData.should.equal(current); 583 | 584 | newRoot.toJS().should.eql({ foo: {}, bar: { b: 24 } }); 585 | struct.current.toJS().should.eql(newRoot.toJS()); 586 | }); 587 | 588 | foo.delete('a'); 589 | 590 | struct.current.toJS().should.eql({ foo: {}, bar: {b: 24} }); 591 | 592 | current = struct.current; 593 | struct.once('swap', function(newRoot, oldRoot) { 594 | oldRoot.toJS().should.eql({ foo: { }, bar: { b: 24 } }); 595 | 596 | current.toJS().should.eql(oldRoot.toJS()); 597 | current.toJS().should.not.eql(newRoot.toJS()); 598 | newRoot.toJS().should.not.eql(oldRoot.toJS()); 599 | 600 | newRoot.toJS().should.eql({ foo: {}, bar: {} }); 601 | }); 602 | 603 | bar.delete('b'); 604 | 605 | struct.current.toJS().should.eql({ foo: {}, bar: {} }); 606 | }); 607 | 608 | }); 609 | 610 | }); 611 | 612 | describe('history', function () { 613 | 614 | it('should be able to undo default', function () { 615 | var structure = new Structure({ 616 | data: { 'foo': 'bar' }, 617 | history: true 618 | }); 619 | 620 | structure.cursor('foo').update(function () { return 'hello'; }); 621 | structure.cursor('foo').deref().should.equal('hello'); 622 | structure.undo(); 623 | structure.cursor('foo').deref().should.equal('bar'); 624 | structure.cursor('foo').update(function () { return 'hello2'; }); 625 | structure.cursor('foo').deref().should.equal('hello2'); 626 | structure.history.toJS().should.eql([ 627 | { 'foo': 'bar' }, 628 | { 'foo': 'hello2' } 629 | ]); 630 | }); 631 | 632 | it('should be able to redo default', function () { 633 | var structure = new Structure({ 634 | data: { 'foo': 'bar' }, 635 | history: true 636 | }); 637 | 638 | structure.cursor('foo').update(function () { return 'hello'; }); 639 | structure.cursor('foo').deref().should.equal('hello'); 640 | structure.undo(); 641 | structure.cursor('foo').deref().should.equal('bar'); 642 | structure.redo(); 643 | structure.cursor('foo').deref().should.equal('hello'); 644 | }); 645 | 646 | it('should be able undo multiple steps', function () { 647 | var structure = new Structure({ 648 | data: { 'foo': 'bar' }, 649 | history: true 650 | }); 651 | 652 | structure.cursor('foo').update(function () { return 'Change 1'; }); 653 | structure.cursor('foo').update(function () { return 'Change 2'; }); 654 | structure.cursor('foo').deref().should.equal('Change 2'); 655 | 656 | structure.undo(2); 657 | structure.cursor('foo').deref().should.equal('bar'); 658 | }); 659 | 660 | it('should be able redo multiple steps', function () { 661 | var structure = new Structure({ 662 | data: { 'foo': 'bar' }, 663 | history: true 664 | }); 665 | 666 | structure.cursor('foo').update(function () { return 'Change 1'; }); 667 | structure.cursor('foo').update(function () { return 'Change 2'; }); 668 | structure.cursor('foo').update(function () { return 'Change 3'; }); 669 | structure.cursor('foo').deref().should.equal('Change 3'); 670 | 671 | structure.undo(3); 672 | structure.cursor('foo').deref().should.equal('bar'); 673 | 674 | structure.redo(2); 675 | structure.cursor('foo').deref().should.equal('Change 2'); 676 | }); 677 | 678 | it('should be able undo until object passed as argument', function () { 679 | var structure = new Structure({ 680 | data: { 'foo': 'bar' }, 681 | history: true 682 | }); 683 | 684 | structure.cursor('foo').update(function () { return 'Change 1'; }); 685 | structure.cursor('foo').deref().should.equal('Change 1'); 686 | var change1 = structure.current; 687 | 688 | structure.cursor('foo').update(function () { return 'Change 2'; }); 689 | structure.cursor('foo').deref().should.equal('Change 2'); 690 | 691 | structure.undoUntil(change1); 692 | structure.cursor('foo').deref().should.equal('Change 1'); 693 | }); 694 | 695 | 696 | describe('with limit', function () { 697 | 698 | it('should be able to undo default', function () { 699 | var structure = new Structure({ 700 | data: { 'foo': 'bar' }, 701 | history: true, 702 | historyLimit: 2 703 | }); 704 | 705 | structure.cursor('foo').update(function () { return 'hello'; }); 706 | structure.cursor('foo').deref().should.equal('hello'); 707 | structure.undo(); 708 | structure.cursor('foo').deref().should.equal('bar'); 709 | structure.cursor('foo').update(function () { return 'hello2'; }); 710 | structure.cursor('foo').deref().should.equal('hello2'); 711 | structure.history.toJS().should.eql([ 712 | { 'foo': 'bar' }, 713 | { 'foo': 'hello2' } 714 | ]); 715 | structure.cursor('foo').update(function () { return 'hello3'; }); 716 | structure.cursor('foo').deref().should.equal('hello3'); 717 | structure.history.toJS().should.eql([ 718 | { 'foo': 'hello2' }, 719 | { 'foo': 'hello3' } 720 | ]); 721 | }); 722 | 723 | it('should be able to redo default', function () { 724 | var structure = new Structure({ 725 | data: { 'foo': 'bar' }, 726 | history: true, 727 | historyLimit: 2 728 | }); 729 | 730 | structure.cursor('foo').update(function () { return 'hello'; }); 731 | structure.cursor('foo').deref().should.equal('hello'); 732 | structure.undo(); 733 | structure.cursor('foo').deref().should.equal('bar'); 734 | structure.redo(); 735 | structure.cursor('foo').deref().should.equal('hello'); 736 | }); 737 | 738 | it('should be able undo multiple steps up to capped start', function () { 739 | var structure = new Structure({ 740 | data: { 'foo': 'bar' }, 741 | history: true, 742 | historyLimit: 2 743 | }); 744 | 745 | structure.cursor('foo').update(function () { return 'Change 1'; }); 746 | structure.cursor('foo').update(function () { return 'Change 2'; }); 747 | structure.cursor('foo').deref().should.equal('Change 2'); 748 | 749 | structure.undo(2); 750 | structure.cursor('foo').deref().should.equal('Change 1'); 751 | }); 752 | 753 | it('should be able redo multiple steps', function () { 754 | var structure = new Structure({ 755 | data: { 'foo': 'bar' }, 756 | history: true, 757 | historyLimit: 2 758 | }); 759 | 760 | structure.cursor('foo').update(function () { return 'Change 1'; }); 761 | structure.cursor('foo').update(function () { return 'Change 2'; }); 762 | structure.cursor('foo').update(function () { return 'Change 3'; }); 763 | structure.cursor('foo').deref().should.equal('Change 3'); 764 | 765 | structure.undo(3); 766 | structure.cursor('foo').deref().should.equal('Change 2'); 767 | 768 | structure.redo(2); 769 | structure.cursor('foo').deref().should.equal('Change 3'); 770 | }); 771 | 772 | }); 773 | 774 | }); 775 | 776 | 777 | describe('reference', function () { 778 | 779 | it('should expose API for creating reference', function () { 780 | var structure = new Structure({ 781 | data: { 'foo': 'bar' } 782 | }); 783 | 784 | structure.should.have.property('reference'); 785 | }); 786 | 787 | it('should expose references with observe and unobserve functions', function () { 788 | var ref = new Structure({ 789 | data: { 'foo': 'bar' } 790 | }).reference(); 791 | 792 | ref.should.have.property('observe'); 793 | ref.should.have.property('unobserveAll'); 794 | }); 795 | 796 | it('should listen to paths before they are created', function (done) { 797 | var structure = new Structure({ data: {} }); 798 | var i = 0; 799 | 800 | var ref = structure.reference('foo'); 801 | ref.observe(function (data) { 802 | if (i++ === 0) { 803 | ref.cursor().deref().should.eql('hello world'); 804 | } else { 805 | ref.cursor().toJS().should.eql({ value: 10 }); 806 | done(); 807 | } 808 | }); 809 | 810 | structure.cursor().update(function() { 811 | return Immutable.fromJS({ foo: 'hello world' }); 812 | }); 813 | 814 | 815 | structure.cursor().update(function() { 816 | return Immutable.fromJS({ foo: { value: 10 } }); 817 | }); 818 | }); 819 | 820 | it('should listen to nested paths before they are created', function (done) { 821 | var structure = new Structure({ data: {} }); 822 | var i = 0; 823 | 824 | var ref = structure.reference(['foo', 'value']); 825 | ref.observe(function () { 826 | ref.cursor().deref().should.eql(10); 827 | done(); 828 | }); 829 | 830 | structure.cursor().update(function() { 831 | return Immutable.fromJS({ foo: 'hello world' }); 832 | }); 833 | 834 | structure.cursor().update(function() { 835 | return Immutable.fromJS({ foo: { value: 10 } }); 836 | }); 837 | }); 838 | 839 | it('should create cursor for value', function () { 840 | var structure = new Structure({ 841 | data: { 'foo': 'bar' } 842 | }); 843 | 844 | structure.reference('foo').cursor().deref().should.equal('bar'); 845 | }); 846 | 847 | it('should have a self-updating cursor', function () { 848 | var structure = new Structure({ 849 | data: { 'foo': 'bar' } 850 | }); 851 | 852 | var ref = structure.reference('foo'); 853 | var newCursor = ref.cursor().update(function () { 854 | return 'updated'; 855 | }); 856 | newCursor.deref().should.equal('updated'); 857 | ref.cursor().deref().should.equal('updated'); 858 | }); 859 | 860 | it('should take cursor as argument', function () { 861 | var structure = new Structure({ 862 | data: { 'foo': 'bar' } 863 | }); 864 | 865 | var ref = structure.reference(structure.cursor('foo')); 866 | var isCalled = false; 867 | var newCursor = ref.cursor().update(function () { 868 | isCalled = true; 869 | return 'updated'; 870 | }); 871 | newCursor.deref().should.equal('updated'); 872 | ref.cursor().deref().should.equal('updated'); 873 | isCalled.should.equal(true); 874 | }); 875 | 876 | it('should have a self-updating cursor when changing from outside', function () { 877 | var structure = new Structure({ 878 | data: { 'foo': 'bar' } 879 | }); 880 | 881 | var ref = structure.reference('foo'); 882 | var newCursor = structure.cursor('foo').update(function () { 883 | return 'updated'; 884 | }); 885 | 886 | newCursor.deref().should.equal('updated'); 887 | ref.cursor().deref().should.equal('updated'); 888 | }); 889 | 890 | it('should have a self-updating cursor on children', function () { 891 | var structure = new Structure({ 892 | data: { 'foo': { 'bar': 1 } } 893 | }); 894 | 895 | var ref = structure.reference('foo'); 896 | var newCursor = ref.cursor().cursor('bar').update(function (state) { 897 | return 'updated'; 898 | }); 899 | newCursor.deref().should.equal('updated'); 900 | ref.cursor().toJS().should.eql({ 901 | 'bar': 'updated' 902 | }); 903 | }); 904 | 905 | it('should support sub-cursor', function () { 906 | var structure = new Structure({ 907 | data: { 'foo': { 'bar': 1 } } 908 | }); 909 | 910 | var ref = structure.reference('foo'); 911 | var newCursor = ref.cursor('bar').update(function (state) { 912 | return 'updated'; 913 | }); 914 | newCursor.deref().should.equal('updated'); 915 | ref.cursor().toJS().should.eql({ 916 | 'bar': 'updated' 917 | }); 918 | }); 919 | 920 | 921 | it('should still be a reference after unobserve', function () { 922 | var structure = new Structure({ 923 | data: { 'foo': 'bar' } 924 | }); 925 | 926 | var ref = structure.reference('foo'); 927 | ref.unobserveAll(); 928 | ref.cursor().update(function () { return 'updated'; }); 929 | ref.cursor().deref().should.equal('updated'); 930 | }); 931 | 932 | it('should be destroyable', function () { 933 | var structure = new Structure({ 934 | data: { 'foo': 'bar' } 935 | }); 936 | 937 | var ref = structure.reference('foo'); 938 | ref.destroy(); 939 | (ref.cursor === void 0).should.equal(true); 940 | (ref.observe === void 0).should.equal(true); 941 | (ref.unobserveAll === void 0).should.equal(true); 942 | }); 943 | 944 | describe('listeners', function () { 945 | 946 | it('should trigger change listener for reference', function (done) { 947 | var structure = new Structure({ 948 | data: { 'foo': 'bar' } 949 | }); 950 | 951 | var ref = structure.reference('foo'); 952 | ref.observe(function () { done(); }); 953 | ref.cursor().update(function () { return 'updated'; }); 954 | }); 955 | 956 | it('should trigger change listener for depending observed references', function (done) { 957 | var structure = new Structure({ 958 | data: {a: {b: {c: "value"}}} 959 | }); 960 | 961 | var ref = structure.reference(['a', 'b']); 962 | ref.observe(function () { done(); }); 963 | structure.cursor('a').update(function (old) { return old.setIn(['b', 'c'], 'updated'); }); 964 | }); 965 | 966 | it('should trigger change listener for depending observed references', function (done) { 967 | var structure = new Structure({ 968 | data: {a: {b: {c: "value"}}} 969 | }); 970 | 971 | var ref = structure.reference(['a', 'b']); 972 | ref.observe(function () { done(); }); 973 | structure.cursor().update(function (old) { return old.setIn(['a', 'b', 'c'], 'updated'); }); 974 | }); 975 | 976 | it('should trigger change listener for reference created from a cursor', function (done) { 977 | var structure = new Structure({ 978 | data: { 'foo': 'bar' } 979 | }); 980 | 981 | var ref = structure.reference(structure.cursor('foo')); 982 | ref.observe(function () { done(); }); 983 | ref.cursor().update(function () { return 'updated'; }); 984 | }); 985 | 986 | it('should trigger change listener for reference when changing cursor from outside', function (done) { 987 | var structure = new Structure({ 988 | data: { 'foo': 'bar' } 989 | }); 990 | 991 | var ref = structure.reference('foo'); 992 | ref.observe(function () { done(); }); 993 | structure.cursor('foo').update(function () { return 'updated'; }); 994 | }); 995 | 996 | it('should trigger change listener for reference when changing cursor from outside on root', function (done) { 997 | var structure = new Structure({ 998 | data: { 'foo': 'bar' } 999 | }); 1000 | 1001 | var ref = structure.reference(); 1002 | ref.observe(function () { done(); }); 1003 | structure.cursor('foo').update(function () { return 'updated'; }); 1004 | }); 1005 | 1006 | it('should trigger change listener for reference created from a reference', function (done) { 1007 | var structure = new Structure({ 1008 | data: { 1009 | 'foo': { 1010 | 'bar': { 1011 | val: 1 1012 | } 1013 | } 1014 | } 1015 | }); 1016 | 1017 | var ref = structure.reference('foo'); 1018 | var ref2 = ref.reference('bar'); 1019 | 1020 | ref2.cursor().toJS().should.eql({ val: 1 }); 1021 | ref2.observe(function () { 1022 | ref2.cursor().toJS().should.eql({ val: 2 }); 1023 | done(); 1024 | }); 1025 | ref.cursor(['bar', 'val']).update(function () { return 2; }); 1026 | }); 1027 | 1028 | it('should support nested paths', function () { 1029 | var structure = new Structure({ 1030 | data: { 1031 | someBox: { message: 'Hello World!' } 1032 | } 1033 | }); 1034 | 1035 | var ref = structure.reference(['someBox', 'message']); 1036 | var newCursor = ref.cursor().update(function () { return 'Hello, World!'; }); 1037 | ref.cursor().deref().should.equal(newCursor.deref()); 1038 | }); 1039 | 1040 | it('should trigger swap', function (done) { 1041 | var structure = new Structure({ 1042 | data: { 'foo': 'bar' } 1043 | }); 1044 | 1045 | var ref = structure.reference('foo'); 1046 | ref.observe('swap', function (newData, oldData, keyPath) { 1047 | keyPath.should.eql([]); 1048 | newData.should.eql('updated'); 1049 | oldData.should.eql('bar'); 1050 | done(); 1051 | }); 1052 | structure.cursor('foo').update(function () { return 'updated'; }); 1053 | }); 1054 | 1055 | it('should trigger swap implicit', function (done) { 1056 | var structure = new Structure({ 1057 | data: { 'foo': 'bar' } 1058 | }); 1059 | 1060 | var ref = structure.reference('foo'); 1061 | ref.observe(function (newData, oldData, keyPath) { 1062 | keyPath.should.eql([]); 1063 | newData.should.eql('updated'); 1064 | oldData.should.eql('bar'); 1065 | done(); 1066 | }); 1067 | structure.cursor('foo').update(function () { return 'updated'; }); 1068 | }); 1069 | 1070 | it('should pass keyPath of the part that has actually changed', function (done) { 1071 | var structure = new Structure({ 1072 | data: { 'foo': { 'bar': 'hello' } } 1073 | }); 1074 | 1075 | var numberOfCalls = 0; 1076 | 1077 | var ref = structure.reference('foo'); 1078 | ref.observe(function (newData, oldData, keyPath) { 1079 | numberOfCalls++; 1080 | keyPath.should.eql(['bar']); 1081 | newData.toJS().should.eql({ 'bar': 'updated' }); 1082 | oldData.toJS().should.eql({ 'bar': 'hello' }); 1083 | }); 1084 | ref.cursor('bar').update(function () { return 'updated'; }); 1085 | 1086 | structure = new Structure({ 1087 | data: { 'foo': { 'bar': 'hello' } } 1088 | }); 1089 | 1090 | var ref2 = structure.reference('foo'); 1091 | ref2.observe(function (newData, oldData, keyPath) { 1092 | numberOfCalls++; 1093 | keyPath.should.eql(['bar']); 1094 | newData.toJS().should.eql({ 'bar': 'updated' }); 1095 | oldData.toJS().should.eql({ 'bar': 'hello' }); 1096 | 1097 | numberOfCalls.should.equal(2); 1098 | done(); 1099 | }); 1100 | ref2.cursor('bar').update(function () { return 'updated'; }); 1101 | }); 1102 | 1103 | it('should pass new and old object and keyPath relative to reference path', function (done) { 1104 | var structure = new Structure({ 1105 | data: { 'foo': { 'bar': 'hello' } } 1106 | }); 1107 | 1108 | var ref = structure.reference(['foo', 'bar']); 1109 | ref.observe(function (newData, oldData, keyPath) { 1110 | keyPath.should.eql([]); 1111 | newData.should.eql('updated'); 1112 | oldData.should.eql('hello'); 1113 | done(); 1114 | }); 1115 | ref.cursor().update(function () { return 'updated'; }); 1116 | }); 1117 | 1118 | it('should trigger any event on any type of add, delete and change for references', function (done) { 1119 | var structure = new Structure({ 1120 | data: { } 1121 | }); 1122 | var ref = structure.reference('foo'); 1123 | 1124 | var numberOfCalls = 0; 1125 | 1126 | ref.observe('any', function (newData, oldData, keyPath) { 1127 | numberOfCalls++; 1128 | 1129 | if (numberOfCalls === 6) { 1130 | done(); 1131 | } 1132 | }); 1133 | 1134 | ref.observe('add', function (newData, keyPath) { 1135 | numberOfCalls++; 1136 | keyPath.should.eql(['foo']); 1137 | newData.should.eql('baz'); 1138 | }); 1139 | ref.cursor().update(function (state) { 1140 | return 'baz'; 1141 | }); 1142 | 1143 | ref.observe('change', function (newData, oldData, keyPath) { 1144 | numberOfCalls++; 1145 | keyPath.should.eql(['foo']); 1146 | newData.should.equal('updated'); 1147 | oldData.should.equal('baz'); 1148 | }); 1149 | structure.cursor('foo').update(function () { return 'updated'; }); 1150 | 1151 | ref.observe('delete', function (oldData, keyPath) { 1152 | numberOfCalls++; 1153 | keyPath.should.eql(['foo']); 1154 | oldData.should.eql('updated'); 1155 | 1156 | if (numberOfCalls === 6) { 1157 | done(); 1158 | } 1159 | }); 1160 | structure.cursor().remove('foo'); 1161 | }); 1162 | 1163 | it('should trigger only change events when specifying event type', function (done) { 1164 | var structure = new Structure({ 1165 | data: { 'foo': 'bar' } 1166 | }); 1167 | 1168 | var ref = structure.reference('foo'); 1169 | ref.observe('delete', function () { expect('Should not be triggered').to.be.false; }); 1170 | ref.observe('add', function () { expect('Should not be triggered').to.be.false; }); 1171 | ref.observe('change', function (newData, oldData, keyPath) { 1172 | keyPath.should.eql(['foo']); 1173 | newData.should.equal('updated'); 1174 | oldData.should.equal('bar'); 1175 | done(); 1176 | }); 1177 | structure.cursor('foo').update(function () { return 'updated'; }); 1178 | }); 1179 | 1180 | it('should trigger reference change event when changed through normal cursor', function (done) { 1181 | var structure = new Structure({ 1182 | data: { foo: 'bar' } 1183 | }); 1184 | 1185 | var ref = structure.reference(['foo', 'bar']); 1186 | ref.observe('change', function (newValue) { 1187 | // this function is not called 1188 | newValue.toJS().should.eql({'bar': 'qux'}); 1189 | done(); 1190 | }); 1191 | structure.cursor(['foo']).update(function () { 1192 | return Immutable.fromJS({'bar': 'qux'}); 1193 | }); 1194 | }) 1195 | 1196 | it('should trigger event type specific listeners on uncreated paths', function (done) { 1197 | var structure = new Structure({ 1198 | data: { } 1199 | }); 1200 | var numCalls = 0; 1201 | 1202 | var ref = structure.reference(['foo', 'bar']); 1203 | ref.observe('add', function (newValue, keyPath) { 1204 | numCalls++; 1205 | keyPath.should.eql(['foo', 'bar']); 1206 | newValue.toJS().should.eql({'baz': 'qux'}); 1207 | }); 1208 | ref.cursor().update(function () { 1209 | return Immutable.fromJS({'baz': 'qux'}); 1210 | }); 1211 | 1212 | structure = new Structure({ data: { } }); 1213 | var ref2 = structure.reference(['foo', 'bar', 'baz']); 1214 | ref2.observe('add', function (newValue, keyPath) { 1215 | numCalls++; 1216 | keyPath.should.eql(['foo', 'bar']); 1217 | newValue.toJS().should.eql({'baz': 'qux'}); 1218 | }); 1219 | structure.cursor(['foo', 'bar']).update(function () { 1220 | return Immutable.fromJS({'baz': 'qux'}); 1221 | }); 1222 | 1223 | structure = new Structure({ data: { } }); 1224 | structure.on('add', function (newValue, keyPath) { 1225 | numCalls++; 1226 | keyPath.should.eql(['foo']); 1227 | newValue.toJS().should.eql({'bar': 'qux'}); 1228 | numCalls.should.equal(3); 1229 | done(); 1230 | }); 1231 | structure.cursor('foo').update(function () { 1232 | return Immutable.fromJS({'bar': 'qux'}); 1233 | }); 1234 | }); 1235 | 1236 | it('should pass new object relative to reference path on specific event type', function (done) { 1237 | var structure = new Structure({ 1238 | data: { 'foo': { 'bar': {} } } 1239 | }); 1240 | 1241 | var ref = structure.reference(['foo', 'bar', 'baz']); 1242 | ref.observe('add', function (newData, keyPath) { 1243 | keyPath.should.eql(['foo', 'bar', 'baz']); 1244 | newData.should.eql('inserted'); 1245 | done(); 1246 | }); 1247 | ref.cursor().update(function () { return 'inserted'; }); 1248 | }); 1249 | 1250 | it('should pass new object relative to reference path but full path on specific event type', function (done) { 1251 | var structure = new Structure({ 1252 | data: { 'foo': { 'bar': {} } } 1253 | }); 1254 | 1255 | var ref = structure.reference('foo'); 1256 | ref.observe('add', function (newData, keyPath) { 1257 | keyPath.should.eql(['foo', 'bar', 'baz']); 1258 | newData.should.eql('inserted'); 1259 | done(); 1260 | }); 1261 | ref.cursor(['bar', 'baz']).update(function () { return 'inserted'; }); 1262 | }); 1263 | 1264 | it('should trigger only delete events when specifying event type', function (done) { 1265 | var structure = new Structure({ 1266 | data: { 'foo': 'bar' } 1267 | }); 1268 | 1269 | var ref = structure.reference('foo'); 1270 | ref.observe('add', function () { expect('Should not be triggered').to.be.false; }); 1271 | ref.observe('change', function () { expect('Should not be triggered').to.be.false; }); 1272 | ref.observe('delete', function (oldData, keyPath) { 1273 | keyPath.should.eql(['foo']); 1274 | oldData.should.eql('bar'); 1275 | done(); 1276 | }); 1277 | structure.cursor().remove('foo'); 1278 | }); 1279 | 1280 | it('should trigger only add events when specifying event type', function (done) { 1281 | var structure = new Structure({ 1282 | data: { 'foo': 'bar' } 1283 | }); 1284 | 1285 | var ref = structure.reference('bar'); 1286 | ref.observe('delete', function () { expect('Should not be triggered').to.be.false; }); 1287 | ref.observe('change', function () { expect('Should not be triggered').to.be.false; }); 1288 | ref.observe('add', function (newData, keyPath) { 1289 | keyPath.should.eql(['bar']); 1290 | newData.should.eql('baz'); 1291 | done(); 1292 | }); 1293 | structure.cursor(['bar']).update(function (state) { 1294 | return 'baz'; 1295 | }); 1296 | }); 1297 | 1298 | it('should trigger on every event type if listening to swap', function (done) { 1299 | var structure = new Structure({ 1300 | data: { 'foo': 'bar' } 1301 | }); 1302 | 1303 | var ref = structure.reference('bar'); 1304 | var i = 0; 1305 | ref.observe('swap', function () {  1306 | i++; 1307 | if (i === 3) done(); 1308 | }); 1309 | 1310 | // Add 1311 | structure.cursor(['bar']).update(function (state) { 1312 | return 'baz'; 1313 | }); 1314 | 1315 | // Change 1316 | structure.cursor('bar').update(function () { return 'updated'; }); 1317 | 1318 | // Delete 1319 | structure.cursor().remove('bar'); 1320 | }); 1321 | 1322 | it('should trigger on every event type if not specified event name', function (done) { 1323 | var structure = new Structure({ 1324 | data: { 'foo': 'bar' } 1325 | }); 1326 | 1327 | var ref = structure.reference('bar'); 1328 | var i = 0; 1329 | ref.observe(function () {  1330 | i++; 1331 | if (i === 3) done(); 1332 | }); 1333 | 1334 | // Add 1335 | structure.cursor(['bar']).update(function (state) { 1336 | return 'baz'; 1337 | }); 1338 | 1339 | // Change 1340 | structure.cursor('bar').update(function () { return 'updated'; }); 1341 | 1342 | // Delete 1343 | structure.cursor().remove('bar'); 1344 | }); 1345 | 1346 | it('should trigger multiple change listeners for reference', function (done) { 1347 | var structure = new Structure({ 1348 | data: { 'foo': 'bar' } 1349 | }); 1350 | 1351 | var ref = structure.reference('foo'); 1352 | 1353 | var i = 0; 1354 | ref.observe(function () { i++; }); 1355 | ref.observe(function () { i++; }); 1356 | ref.observe(function () { if(i == 2) done(); }); 1357 | ref.cursor().update(function () { return 'updated'; }); 1358 | }); 1359 | 1360 | it('should not trigger removed listener', function (done) { 1361 | var structure = new Structure({ 1362 | data: { 'foo': 'bar' } 1363 | }); 1364 | 1365 | var i = 0; 1366 | var ref = structure.reference('foo'); 1367 | var unsubscribe = ref.observe(function () { i++; }); 1368 | unsubscribe(); 1369 | 1370 | ref.observe(function () { i++; }); 1371 | ref.observe(function () { if(i == 1) done(); }); 1372 | ref.cursor().update(function () { return 'updated'; }); 1373 | }); 1374 | 1375 | 1376 | it('should not remove listeners from other cursors', function (done) { 1377 | var structure = new Structure({data: {foo: 'bar'}}); 1378 | var i = 0; 1379 | var i2 = 0; 1380 | var ref = structure.reference('foo'); 1381 | var ref2 = structure.reference('foo'); 1382 | ref.observe(function () { i++; }); 1383 | ref2.observe(function () { i2++; }); 1384 | ref2.cursor().update(function () { return 'updated'; }); 1385 | ref2.observe(function () { if(i == 1 && i2 == 2) done(); }); 1386 | ref.unobserveAll(); 1387 | ref.cursor().update(function () { return 'updated again'; }); 1388 | }); 1389 | 1390 | it('should be able to call unsubscribe multiple times without effect', function (done) { 1391 | var structure = new Structure({ 1392 | data: { 'foo': 'bar' } 1393 | }); 1394 | 1395 | var ref = structure.reference('foo'); 1396 | 1397 | var i = 0; 1398 | var unsubscribe = ref.observe(function () { i++; }); 1399 | unsubscribe(); 1400 | unsubscribe(); 1401 | unsubscribe(); 1402 | 1403 | ref.observe(function () { i++; }); 1404 | ref.observe(function () { if(i == 1) done(); }); 1405 | ref.cursor().update(function () { return 'updated'; }); 1406 | }); 1407 | 1408 | it('should be able to call add new listeners after unsubscribing all', function (done) { 1409 | var structure = new Structure({ 1410 | data: { 'foo': 'bar' } 1411 | }); 1412 | 1413 | var ref = structure.reference('foo'); 1414 | 1415 | var i = 0; 1416 | ref.observe(function () { i++; }); 1417 | ref.unobserveAll(); 1418 | 1419 | ref.observe(function () { i++; }); 1420 | ref.observe(function () {  1421 | i.should.equal(1); 1422 | ref.cursor().deref().should.equal('updated'); 1423 | done(); 1424 | }); 1425 | ref.cursor().update(function () { return 'updated'; }); 1426 | }); 1427 | 1428 | it('should not remove new listeners with old unsubscribers', function (done) { 1429 | var structure = new Structure({ 1430 | data: { 'foo': 'bar' } 1431 | }); 1432 | 1433 | var ref = structure.reference('foo'); 1434 | var changed = false; 1435 | var unsubscribe = ref.observe(function () { changed = true; }); 1436 | unsubscribe(); 1437 | 1438 | ref.observe(function () {  1439 | changed.should.equal(false); 1440 | done(); 1441 | }); 1442 | unsubscribe(); 1443 | ref.cursor().update(function () { return 'updated'; }); 1444 | }); 1445 | 1446 | it('should not trigger multiple removed listener', function (done) { 1447 | var structure = new Structure({ 1448 | data: { 'foo': 'bar' } 1449 | }); 1450 | 1451 | var ref = structure.reference('foo'); 1452 | 1453 | var i = 0; 1454 | var unsubscribe = ref.observe(function () { i++; }); 1455 | unsubscribe(); 1456 | 1457 | unsubscribe = ref.observe(function () { i++; }); 1458 | unsubscribe(); 1459 | 1460 | ref.observe(function () { if(i == 0) done(); }); 1461 | ref.cursor().update(function () { return 'updated'; }); 1462 | }); 1463 | 1464 | it('should unsubscribe all listeners for path', function () { 1465 | var structure = new Structure({ 1466 | data: { 'foo': 'bar' } 1467 | }); 1468 | 1469 | var changed = false; 1470 | var ref = structure.reference('foo'); 1471 | var cursor = ref.cursor(); 1472 | 1473 | ref.observe(function () { changed = true; }); 1474 | ref.observe(function () { changed = true; }); 1475 | ref.observe(function () { changed = true; }); 1476 | 1477 | ref.unobserveAll(); 1478 | cursor.update(function() { return 'changed'; }); 1479 | changed.should.equal(false); 1480 | }); 1481 | 1482 | it('should only remove listeners on given path', function () { 1483 | var structure = new Structure({ 1484 | data: { 'foo': 'bar', 'bar': 'foo' } 1485 | }); 1486 | 1487 | var ref1 = structure.reference('foo'); 1488 | var ref2 = structure.reference('bar'); 1489 | var cursor1 = ref1.cursor(); 1490 | var cursor2 = ref2.cursor(); 1491 | 1492 | var firstChange = false; 1493 | var secondChange = false; 1494 | 1495 | ref1.observe(function () { firstChange = true; }); 1496 | ref1.observe(function () { firstChange = true; }); 1497 | ref1.observe(function () { firstChange = true; }); 1498 | 1499 | ref2.observe(function () { secondChange = true; }); 1500 | 1501 | ref1.unobserveAll(); 1502 | cursor1.update(function() { return 'changed'; }); 1503 | firstChange.should.equal(false); 1504 | 1505 | cursor2.update(function() { return 'changed'; }); 1506 | secondChange.should.equal(true); 1507 | }); 1508 | 1509 | it('should remove listeners for all local references', function () { 1510 | var structure = new Structure({ 1511 | data: { 'foo': 'bar', 'bar': 'foo' } 1512 | }); 1513 | 1514 | var ref1 = structure.reference('foo'); 1515 | var ref2 = structure.reference('bar'); 1516 | var cursor1 = ref1.cursor(); 1517 | var cursor2 = ref2.cursor(); 1518 | 1519 | var firstChange = false; 1520 | var secondChange = false; 1521 | 1522 | ref1.observe(function () { firstChange = true; }); 1523 | ref1.observe(function () { firstChange = true; }); 1524 | ref1.observe(function () { firstChange = true; }); 1525 | 1526 | 1527 | ref1.unobserveAll(); 1528 | cursor1.update(function() { return 'changed'; }); 1529 | firstChange.should.equal(false); 1530 | 1531 | ref2.observe(function () { secondChange = true; }); 1532 | ref2.unobserveAll(); 1533 | cursor2.update(function() { return 'changed'; }); 1534 | secondChange.should.equal(false); 1535 | }); 1536 | 1537 | }); 1538 | 1539 | }); 1540 | 1541 | }); 1542 | --------------------------------------------------------------------------------