├── .gitignore ├── .jshintrc ├── .travis.yml ├── CONTRIBUTION.md ├── LICENCE ├── README.md ├── add-listener.js ├── apply-patch.js ├── array-methods.js ├── array-reverse.js ├── array-sort.js ├── index.js ├── lib └── set-non-enumerable.js ├── package.json ├── put.js ├── set.js ├── splice.js ├── test ├── index.js ├── reverse.js └── sort.js └── transaction.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .monitor 3 | .*.swp 4 | .nodemonignore 5 | releases 6 | *.log 7 | *.err 8 | fleet.json 9 | public/browserify 10 | bin/*.json 11 | .bin 12 | build 13 | compile 14 | .lock-wscript 15 | coverage 16 | node_modules 17 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxdepth": 4, 3 | "maxstatements": 200, 4 | "maxcomplexity": 12, 5 | "maxlen": 80, 6 | "maxparams": 5, 7 | 8 | "curly": true, 9 | "eqeqeq": true, 10 | "immed": true, 11 | "latedef": false, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": true, 15 | "undef": true, 16 | "unused": "vars", 17 | "trailing": true, 18 | 19 | "quotmark": true, 20 | "expr": true, 21 | "asi": true, 22 | 23 | "browser": false, 24 | "esnext": true, 25 | "devel": false, 26 | "node": false, 27 | "nonstandard": false, 28 | 29 | "predef": ["require", "module", "__dirname", "__filename"] 30 | } 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - "0.10" 5 | before_script: 6 | - npm install 7 | - npm install istanbul coveralls 8 | script: npm run travis-test 9 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # This is an OPEN Open Source Project 2 | 3 | ## What? 4 | 5 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. 6 | 7 | ## Rules 8 | 9 | There are a few basic ground-rules for contributors: 10 | 11 | - No `--force` pushes or modifying the Git history in any way. 12 | - Non-master branches ought to be used for ongoing work. 13 | - External API changes and significant modifications ought to be subject to an internal pull-request to solicit feedback from other contributors. 14 | - Internal pull-requests to solicit feedback are encouraged for any other non-trivial contribution but left to the discretion of the contributor. 15 | - For significant changes wait a full 24 hours before merging so that active contributors who are distributed throughout the world have a chance to weigh in. 16 | - Contributors should attempt to adhere to the prevailing code-style. 17 | 18 | ## Releases 19 | 20 | Declaring formal releases requires peer review. 21 | 22 | - A reviewer of a pull request should recommend a new version number (patch, minor or major). 23 | - Once your change is merged feel free to bump the version as recommended by the reviewer. 24 | - A new version number should not be cut without peer review unless done by the project maintainer. 25 | 26 | ## Want to contribute? 27 | 28 | Even though collaborators may contribute as they see fit, if you are not sure what to do, here's a suggested process: 29 | 30 | ### Cutting a new version 31 | 32 | - Get your branch merged on master 33 | - Run `npm version major` or `npm version minor` or `npm version patch` 34 | - `git push origin master --tags` 35 | - If you are a project owner, then `npm publish` 36 | 37 | ### If you want to have a bug fixed or a feature added: 38 | 39 | - Check open issues for what you want. 40 | - If there is an open issue, comment on it, otherwise open an issue describing your bug or feature with use cases. 41 | - Discussion happens on the issue about how to solve your problem. 42 | - You or a core contributor opens a pull request solving the issue with tests and documentation. 43 | - The pull requests gets reviewed and then merged. 44 | - A new release version get's cut. 45 | - (Disclaimer: Your feature might get rejected.) 46 | 47 | ### Changes to this arrangement 48 | 49 | This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. 50 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Raynos. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # observ-array 2 | 3 | 10 | 11 | 12 | 13 | An array containing observable values 14 | 15 | ## Example 16 | 17 | An `ObservArray` is an observable version of an array, every 18 | mutation of the array or mutation of an observable element in 19 | the array will cause the `ObservArray` to emit a new changed 20 | plain javascript array. 21 | 22 | ```js 23 | var ObservArray = require("observ-array") 24 | var ObservStruct = require("observ-struct") 25 | var Observ = require("observ") 26 | var uuid = require("uuid") 27 | 28 | function createTodo(title) { 29 | return ObservStruct({ 30 | id: uuid(), 31 | title: Observ(title || ""), 32 | completed: Observ(false) 33 | }) 34 | } 35 | 36 | var state = ObservStruct({ 37 | todos: ObservArray([ 38 | createTodo("some todo"), 39 | createTodo("some other todo") 40 | ]) 41 | }) 42 | 43 | state(function (currState) { 44 | // currState.todos is a plain javascript todo 45 | // currState.todos[0] is a plain javascript value 46 | currState.todos.forEach(function (todo, index) { 47 | console.log("todo", todo.title, index) 48 | }) 49 | }) 50 | 51 | state.todos.get(0).title.set("some new title") 52 | state.todos.push(createTodo("another todo")) 53 | ``` 54 | 55 | ### Transactions 56 | 57 | Batch changes together with transactions. 58 | 59 | ```js 60 | var array = ObservArray([ Observ("foo"), Observ("bar") ]) 61 | 62 | var removeListener = array(handleChange) 63 | 64 | array.transaction(function(rawList) { 65 | rawList.push(Observ("foobar")) 66 | rawList.splice(1, 1, Observ("baz"), Observ("bazbar")) 67 | rawList.unshift(Observ("foobaz")) 68 | rawList[6] = Observ("foobarbaz") 69 | }) 70 | 71 | function handleChange(value) { 72 | // this will only be called once 73 | // changes are batched into a single diff 74 | value._diff //= [ [1,1,"baz","bazbar","foobar", , "foobarbaz"], 75 | // [0,0,"foobaz"] ] 76 | } 77 | ``` 78 | 79 | ## Installation 80 | 81 | `npm install observ-array` 82 | 83 | ## Contributors 84 | 85 | - Raynos 86 | - [Matt McKegg][13] 87 | 88 | ## MIT Licenced 89 | 90 | [1]: https://secure.travis-ci.org/Raynos/observ-array.png 91 | [2]: https://travis-ci.org/Raynos/observ-array 92 | [3]: https://badge.fury.io/js/observ-array.png 93 | [4]: https://badge.fury.io/js/observ-array 94 | [5]: https://coveralls.io/repos/Raynos/observ-array/badge.png 95 | [6]: https://coveralls.io/r/Raynos/observ-array 96 | [7]: https://gemnasium.com/Raynos/observ-array.png 97 | [8]: https://gemnasium.com/Raynos/observ-array 98 | [9]: https://david-dm.org/Raynos/observ-array.png 99 | [10]: https://david-dm.org/Raynos/observ-array 100 | [11]: https://ci.testling.com/Raynos/observ-array.png 101 | [12]: https://ci.testling.com/Raynos/observ-array 102 | [13]: https://github.com/mmckegg 103 | -------------------------------------------------------------------------------- /add-listener.js: -------------------------------------------------------------------------------- 1 | var setNonEnumerable = require("./lib/set-non-enumerable.js"); 2 | 3 | module.exports = addListener 4 | 5 | function addListener(observArray, observ) { 6 | var list = observArray._list 7 | 8 | return observ(function (value) { 9 | var valueList = observArray().slice() 10 | var index = list.indexOf(observ) 11 | 12 | // This code path should never hit. If this happens 13 | // there's a bug in the cleanup code 14 | if (index === -1) { 15 | var message = "observ-array: Unremoved observ listener" 16 | var err = new Error(message) 17 | err.list = list 18 | err.index = index 19 | err.observ = observ 20 | throw err 21 | } 22 | 23 | valueList.splice(index, 1, value) 24 | setNonEnumerable(valueList, "_diff", [ [index, 1, value] ]) 25 | 26 | observArray._observSet(valueList) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /apply-patch.js: -------------------------------------------------------------------------------- 1 | var addListener = require('./add-listener.js') 2 | 3 | module.exports = applyPatch 4 | 5 | function applyPatch (valueList, args) { 6 | var obs = this 7 | var valueArgs = args.map(unpack) 8 | 9 | valueList.splice.apply(valueList, valueArgs) 10 | obs._list.splice.apply(obs._list, args) 11 | 12 | var extraRemoveListeners = args.slice(2).map(function (observ) { 13 | return typeof observ === "function" ? 14 | addListener(obs, observ) : 15 | null 16 | }) 17 | 18 | extraRemoveListeners.unshift(args[0], args[1]) 19 | var removedListeners = obs._removeListeners.splice 20 | .apply(obs._removeListeners, extraRemoveListeners) 21 | 22 | removedListeners.forEach(function (removeObservListener) { 23 | if (removeObservListener) { 24 | removeObservListener() 25 | } 26 | }) 27 | 28 | return valueArgs 29 | } 30 | 31 | function unpack(value, index){ 32 | if (index === 0 || index === 1) { 33 | return value 34 | } 35 | return typeof value === "function" ? value() : value 36 | } 37 | -------------------------------------------------------------------------------- /array-methods.js: -------------------------------------------------------------------------------- 1 | var ObservArray = require("./index.js") 2 | 3 | var slice = Array.prototype.slice 4 | 5 | var ARRAY_METHODS = [ 6 | "concat", "slice", "every", "filter", "forEach", "indexOf", 7 | "join", "lastIndexOf", "map", "reduce", "reduceRight", 8 | "some", "toString", "toLocaleString" 9 | ] 10 | 11 | var methods = ARRAY_METHODS.map(function (name) { 12 | return [name, function () { 13 | var res = this._list[name].apply(this._list, arguments) 14 | 15 | if (res && Array.isArray(res)) { 16 | res = ObservArray(res) 17 | } 18 | 19 | return res 20 | }] 21 | }) 22 | 23 | module.exports = ArrayMethods 24 | 25 | function ArrayMethods(obs) { 26 | obs.push = observArrayPush 27 | obs.pop = observArrayPop 28 | obs.shift = observArrayShift 29 | obs.unshift = observArrayUnshift 30 | obs.reverse = require("./array-reverse.js") 31 | obs.sort = require("./array-sort.js") 32 | 33 | methods.forEach(function (tuple) { 34 | obs[tuple[0]] = tuple[1] 35 | }) 36 | return obs 37 | } 38 | 39 | 40 | 41 | function observArrayPush() { 42 | var args = slice.call(arguments) 43 | args.unshift(this._list.length, 0) 44 | this.splice.apply(this, args) 45 | 46 | return this._list.length 47 | } 48 | function observArrayPop() { 49 | return this.splice(this._list.length - 1, 1)[0] 50 | } 51 | function observArrayShift() { 52 | return this.splice(0, 1)[0] 53 | } 54 | function observArrayUnshift() { 55 | var args = slice.call(arguments) 56 | args.unshift(0, 0) 57 | this.splice.apply(this, args) 58 | 59 | return this._list.length 60 | } 61 | 62 | 63 | function notImplemented() { 64 | throw new Error("Pull request welcome") 65 | } 66 | -------------------------------------------------------------------------------- /array-reverse.js: -------------------------------------------------------------------------------- 1 | var applyPatch = require("./apply-patch.js") 2 | var setNonEnumerable = require('./lib/set-non-enumerable.js') 3 | 4 | module.exports = reverse 5 | 6 | function reverse() { 7 | var obs = this 8 | var changes = fakeDiff(obs._list.slice().reverse()) 9 | var valueList = obs().slice().reverse() 10 | 11 | var valueChanges = changes.map(applyPatch.bind(obs, valueList)) 12 | 13 | setNonEnumerable(valueList, "_diff", valueChanges) 14 | 15 | obs._observSet(valueList) 16 | return changes 17 | } 18 | 19 | function fakeDiff(arr) { 20 | var _diff 21 | var len = arr.length 22 | 23 | if(len % 2) { 24 | var midPoint = (len -1) / 2 25 | var a = [0, midPoint].concat(arr.slice(0, midPoint)) 26 | var b = [midPoint +1, midPoint].concat(arr.slice(midPoint +1, len)) 27 | var _diff = [a, b] 28 | } else { 29 | _diff = [ [0, len].concat(arr) ] 30 | } 31 | 32 | return _diff 33 | } 34 | -------------------------------------------------------------------------------- /array-sort.js: -------------------------------------------------------------------------------- 1 | var applyPatch = require("./apply-patch.js") 2 | var setNonEnumerable = require("./lib/set-non-enumerable.js") 3 | 4 | module.exports = sort 5 | 6 | function sort(compare) { 7 | var obs = this 8 | var list = obs._list.slice() 9 | 10 | var unpacked = unpack(list) 11 | 12 | var sorted = unpacked 13 | .map(function(it) { return it.val }) 14 | .sort(compare) 15 | 16 | var packed = repack(sorted, unpacked) 17 | 18 | //fake diff - for perf 19 | //adiff on 10k items === ~3200ms 20 | //fake on 10k items === ~110ms 21 | var changes = [ [ 0, packed.length ].concat(packed) ] 22 | 23 | var valueChanges = changes.map(applyPatch.bind(obs, sorted)) 24 | 25 | setNonEnumerable(sorted, "_diff", valueChanges) 26 | 27 | obs._observSet(sorted) 28 | return changes 29 | } 30 | 31 | function unpack(list) { 32 | var unpacked = [] 33 | for(var i = 0; i < list.length; i++) { 34 | unpacked.push({ 35 | val: ("function" == typeof list[i]) ? list[i]() : list[i], 36 | obj: list[i] 37 | }) 38 | } 39 | return unpacked 40 | } 41 | 42 | function repack(sorted, unpacked) { 43 | var packed = [] 44 | 45 | while(sorted.length) { 46 | var s = sorted.shift() 47 | var indx = indexOf(s, unpacked) 48 | if(~indx) packed.push(unpacked.splice(indx, 1)[0].obj) 49 | } 50 | 51 | return packed 52 | } 53 | 54 | function indexOf(n, h) { 55 | for(var i = 0; i < h.length; i++) { 56 | if(n === h[i].val) return i 57 | } 58 | return -1 59 | } 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Observ = require("observ") 2 | 3 | // circular dep between ArrayMethods & this file 4 | module.exports = ObservArray 5 | 6 | var splice = require("./splice.js") 7 | var put = require("./put.js") 8 | var set = require("./set.js") 9 | var transaction = require("./transaction.js") 10 | var ArrayMethods = require("./array-methods.js") 11 | var addListener = require("./add-listener.js") 12 | 13 | 14 | /* ObservArray := (Array) => Observ< 15 | Array & { _diff: Array } 16 | > & { 17 | splice: (index: Number, amount: Number, rest...: T) => 18 | Array, 19 | push: (values...: T) => Number, 20 | filter: (lambda: Function, thisValue: Any) => Array, 21 | indexOf: (item: T, fromIndex: Number) => Number 22 | } 23 | 24 | Fix to make it more like ObservHash. 25 | 26 | I.e. you write observables into it. 27 | reading methods take plain JS objects to read 28 | and the value of the array is always an array of plain 29 | objsect. 30 | 31 | The observ array instance itself would have indexed 32 | properties that are the observables 33 | */ 34 | function ObservArray(initialList) { 35 | // list is the internal mutable list observ instances that 36 | // all methods on `obs` dispatch to. 37 | var list = initialList 38 | var initialState = [] 39 | 40 | // copy state out of initialList into initialState 41 | list.forEach(function (observ, index) { 42 | initialState[index] = typeof observ === "function" ? 43 | observ() : observ 44 | }) 45 | 46 | var obs = Observ(initialState) 47 | obs.splice = splice 48 | 49 | // override set and store original for later use 50 | obs._observSet = obs.set 51 | obs.set = set 52 | 53 | obs.get = get 54 | obs.getLength = getLength 55 | obs.put = put 56 | obs.transaction = transaction 57 | 58 | // you better not mutate this list directly 59 | // this is the list of observs instances 60 | obs._list = list 61 | 62 | var removeListeners = list.map(function (observ) { 63 | return typeof observ === "function" ? 64 | addListener(obs, observ) : 65 | null 66 | }); 67 | // this is a list of removal functions that must be called 68 | // when observ instances are removed from `obs.list` 69 | // not calling this means we do not GC our observ change 70 | // listeners. Which causes rage bugs 71 | obs._removeListeners = removeListeners 72 | 73 | obs._type = "observ-array" 74 | obs._version = "3" 75 | 76 | return ArrayMethods(obs, list) 77 | } 78 | 79 | function get(index) { 80 | return this._list[index] 81 | } 82 | 83 | function getLength() { 84 | return this._list.length 85 | } 86 | -------------------------------------------------------------------------------- /lib/set-non-enumerable.js: -------------------------------------------------------------------------------- 1 | module.exports = setNonEnumerable; 2 | 3 | function setNonEnumerable(object, key, value) { 4 | Object.defineProperty(object, key, { 5 | value: value, 6 | writable: true, 7 | configurable: true, 8 | enumerable: false 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "observ-array", 3 | "version": "3.2.1", 4 | "description": "An array containing observable values", 5 | "keywords": [], 6 | "author": "Raynos ", 7 | "repository": "git://github.com/Raynos/observ-array.git", 8 | "main": "index", 9 | "homepage": "https://github.com/Raynos/observ-array", 10 | "contributors": [ 11 | { 12 | "name": "Raynos" 13 | } 14 | ], 15 | "bugs": { 16 | "url": "https://github.com/Raynos/observ-array/issues", 17 | "email": "raynos2@gmail.com" 18 | }, 19 | "dependencies": { 20 | "adiff": "^0.2.12", 21 | "observ": "~0.2.0", 22 | "xtend": "^3.0.0" 23 | }, 24 | "devDependencies": { 25 | "observ-hash": "^2.0.0", 26 | "observ-struct": "^6.0.0", 27 | "observ-varhash": "^1.0.4", 28 | "tape": "~2.5.0" 29 | }, 30 | "licenses": [ 31 | { 32 | "type": "MIT", 33 | "url": "http://github.com/Raynos/observ-array/raw/master/LICENSE" 34 | } 35 | ], 36 | "scripts": { 37 | "test": "node ./test/index.js", 38 | "start": "node ./index.js", 39 | "watch": "nodemon -w ./index.js index.js", 40 | "travis-test": "istanbul cover ./test/index.js && ((cat coverage/lcov.info | coveralls) || exit 0)", 41 | "cover": "istanbul cover --report none --print detail ./test/index.js", 42 | "view-cover": "istanbul report html && google-chrome ./coverage/index.html", 43 | "test-browser": "testem-browser ./test/browser/index.js", 44 | "testem": "testem-both -b=./test/browser/index.js" 45 | }, 46 | "testling": { 47 | "files": "test/index.js", 48 | "browsers": [ 49 | "ie/8..latest", 50 | "firefox/16..latest", 51 | "firefox/nightly", 52 | "chrome/22..latest", 53 | "chrome/canary", 54 | "opera/12..latest", 55 | "opera/next", 56 | "safari/5.1..latest", 57 | "ipad/6.0..latest", 58 | "iphone/6.0..latest", 59 | "android-browser/4.2..latest" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /put.js: -------------------------------------------------------------------------------- 1 | var addListener = require("./add-listener.js") 2 | var setNonEnumerable = require("./lib/set-non-enumerable.js"); 3 | 4 | module.exports = put 5 | 6 | // `obs.put` is a mutable implementation of `array[index] = value` 7 | // that mutates both `list` and the internal `valueList` that 8 | // is the current value of `obs` itself 9 | function put(index, value) { 10 | var obs = this 11 | var valueList = obs().slice() 12 | 13 | var originalLength = valueList.length 14 | valueList[index] = typeof value === "function" ? value() : value 15 | 16 | obs._list[index] = value 17 | 18 | // remove past value listener if was observ 19 | var removeListener = obs._removeListeners[index] 20 | if (removeListener){ 21 | removeListener() 22 | } 23 | 24 | // add listener to value if observ 25 | obs._removeListeners[index] = typeof value === "function" ? 26 | addListener(obs, value) : 27 | null 28 | 29 | // fake splice diff 30 | var valueArgs = index < originalLength ? 31 | [index, 1, valueList[index]] : 32 | [index, 0, valueList[index]] 33 | 34 | setNonEnumerable(valueList, "_diff", [valueArgs]) 35 | 36 | obs._observSet(valueList) 37 | return value 38 | } -------------------------------------------------------------------------------- /set.js: -------------------------------------------------------------------------------- 1 | var applyPatch = require("./apply-patch.js") 2 | var setNonEnumerable = require("./lib/set-non-enumerable.js") 3 | var adiff = require("adiff") 4 | 5 | module.exports = set 6 | 7 | function set(rawList) { 8 | if (!Array.isArray(rawList)) rawList = [] 9 | 10 | var obs = this 11 | var changes = adiff.diff(obs._list, rawList) 12 | var valueList = obs().slice() 13 | 14 | var valueChanges = changes.map(applyPatch.bind(obs, valueList)) 15 | 16 | setNonEnumerable(valueList, "_diff", valueChanges) 17 | 18 | obs._observSet(valueList) 19 | return changes 20 | } 21 | -------------------------------------------------------------------------------- /splice.js: -------------------------------------------------------------------------------- 1 | var slice = Array.prototype.slice 2 | 3 | var addListener = require("./add-listener.js") 4 | var setNonEnumerable = require("./lib/set-non-enumerable.js"); 5 | 6 | module.exports = splice 7 | 8 | // `obs.splice` is a mutable implementation of `splice()` 9 | // that mutates both `list` and the internal `valueList` that 10 | // is the current value of `obs` itself 11 | function splice(index, amount) { 12 | var obs = this 13 | var args = slice.call(arguments, 0) 14 | var valueList = obs().slice() 15 | 16 | // generate a list of args to mutate the internal 17 | // list of only obs 18 | var valueArgs = args.map(function (value, index) { 19 | if (index === 0 || index === 1) { 20 | return value 21 | } 22 | 23 | // must unpack observables that we are adding 24 | return typeof value === "function" ? value() : value 25 | }) 26 | 27 | valueList.splice.apply(valueList, valueArgs) 28 | // we remove the observs that we remove 29 | var removed = obs._list.splice.apply(obs._list, args) 30 | 31 | var extraRemoveListeners = args.slice(2).map(function (observ) { 32 | return typeof observ === "function" ? 33 | addListener(obs, observ) : 34 | null 35 | }) 36 | extraRemoveListeners.unshift(args[0], args[1]) 37 | var removedListeners = obs._removeListeners.splice 38 | .apply(obs._removeListeners, extraRemoveListeners) 39 | 40 | removedListeners.forEach(function (removeObservListener) { 41 | if (removeObservListener) { 42 | removeObservListener() 43 | } 44 | }) 45 | 46 | setNonEnumerable(valueList, "_diff", [valueArgs]) 47 | 48 | obs._observSet(valueList) 49 | return removed 50 | } 51 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require("tape") 2 | var Observ = require("observ") 3 | var computed = require("observ/computed") 4 | 5 | var ObservArray = require("../index") 6 | 7 | test("ObservArray is a function", function (assert) { 8 | assert.equal(typeof ObservArray, "function") 9 | assert.end() 10 | }) 11 | 12 | test("ObservArray contains correct initial value", function (assert) { 13 | var arr = ObservArray([ 14 | Observ("foo"), 15 | Observ("bar"), 16 | Observ("baz"), 17 | "foobar" 18 | ]) 19 | var initial = arr() 20 | 21 | assert.equal(typeof arr.filter, "function") 22 | assert.equal(typeof arr.splice, "function") 23 | assert.equal(initial.length, 4) 24 | assert.deepEqual(initial, ["foo", "bar", "baz", "foobar"]) 25 | 26 | assert.equal(arr[0], undefined) 27 | assert.equal(arr[1], undefined) 28 | assert.notEqual(arr.length, 4) 29 | 30 | assert.end() 31 | }) 32 | 33 | test("ObservArray emits change", function (assert) { 34 | var arr = ObservArray([ 35 | Observ("foo"), 36 | Observ("bar") 37 | ]) 38 | var initArr = arr() 39 | var changes = [] 40 | 41 | arr(function (state) { 42 | changes.push(state) 43 | }) 44 | 45 | arr.get(0).set("foo2") 46 | arr.get(1).set("bar2") 47 | 48 | assert.equal(changes.length, 2) 49 | assert.deepEqual(initArr, ["foo", "bar"]) 50 | assert.notEqual(initArr, changes[0]) 51 | assert.notEqual(changes[0], changes[1]) 52 | assert.ok(changes[0]._diff) 53 | assert.equal(Object.keys(changes[0]).indexOf("_diff"), -1) 54 | assert.deepEqual(changes[0]._diff, [ [0, 1, "foo2"] ]) 55 | assert.deepEqual(changes[0].slice(), ["foo2", "bar"]) 56 | assert.deepEqual(changes[1].slice(), ["foo2", "bar2"]) 57 | 58 | assert.end() 59 | }) 60 | 61 | test("works with nested arrays", function (assert) { 62 | var arr = ObservArray([ 63 | Observ("foo"), 64 | ObservArray([ 65 | Observ("bar"), 66 | Observ("baz") 67 | ]) 68 | ]) 69 | var initArr = arr() 70 | var changes = [] 71 | var innerChanges = [] 72 | 73 | arr(function (state) { 74 | changes.push(state) 75 | }) 76 | 77 | arr.get(1)(function (state) { 78 | innerChanges.push(state) 79 | }) 80 | 81 | arr.get(1).get(0).set("bar2") 82 | arr.get(0).set("foo2") 83 | arr.get(1).get(1).set("baz2") 84 | 85 | assert.equal(changes.length, 3) 86 | assert.equal(innerChanges.length, 2) 87 | 88 | assert.notEqual(changes[0], initArr) 89 | assert.notEqual(changes[1], changes[0]) 90 | assert.notEqual(changes[2], changes[1]) 91 | 92 | assert.notEqual(innerChanges[0], initArr[1]) 93 | assert.notEqual(innerChanges[1], innerChanges[0]) 94 | 95 | assert.deepEqual(initArr, [ 96 | "foo", 97 | ["bar", "baz"] 98 | ]) 99 | assert.equal(changes[0][0], "foo") 100 | assert.deepEqual(changes[0][1].slice(), ["bar2", "baz"]) 101 | assert.equal(changes[1][0], "foo2") 102 | assert.deepEqual(changes[1][1].slice(), ["bar2", "baz"]) 103 | assert.equal(changes[2][0], "foo2") 104 | assert.deepEqual(changes[2][1].slice(), ["bar2", "baz2"]) 105 | 106 | assert.deepEqual(initArr[1], ["bar", "baz"]) 107 | assert.deepEqual(innerChanges[0].slice(), ["bar2", "baz"]) 108 | assert.deepEqual(innerChanges[1].slice(), ["bar2", "baz2"]) 109 | 110 | assert.equal(changes[0][1], changes[1][1], 111 | "unchanged properties are the same value") 112 | 113 | assert.end() 114 | }) 115 | 116 | test("can call array methods on value inside", function (assert) { 117 | var arr = ObservArray([ Observ("foo"), Observ("bar") ]) 118 | 119 | var v = arr() 120 | 121 | var list = v.slice() 122 | var doubles = list.map(function (v) { 123 | return v + v 124 | }) 125 | 126 | assert.ok(Array.isArray(v)) 127 | assert.deepEqual(doubles, [ "foofoo", "barbar" ]) 128 | 129 | assert.end() 130 | }) 131 | 132 | test("can call array methods on ObservArray", function (assert) { 133 | var arr = ObservArray([ 134 | Observ(0), 135 | Observ(1), 136 | Observ(2), 137 | Observ(3), 138 | Observ(5) 139 | ]) 140 | 141 | var doubles = arr.map(function (o) { 142 | return computed([o], function (o) { return o * 2 }) 143 | }) 144 | var changes = [] 145 | 146 | doubles(function (state) { 147 | changes.push(state) 148 | }) 149 | 150 | assert.equal(typeof doubles.get, "function") 151 | assert.equal(typeof doubles.getLength, "function") 152 | assert.equal(Array.isArray(doubles), false) 153 | 154 | arr.get(2).set(5) 155 | 156 | assert.equal(changes.length, 1) 157 | assert.deepEqual(changes[0].slice(), [ 158 | 0, 2, 10, 6, 10 159 | ]) 160 | 161 | doubles.push(Observ(8)) 162 | 163 | assert.equal(changes.length, 2) 164 | assert.deepEqual(changes[1].slice(), [ 165 | 0, 2, 10, 6, 10, 8 166 | ]) 167 | 168 | assert.end() 169 | }) 170 | 171 | test("can add values to observ array", function (assert) { 172 | var arr = ObservArray([ 173 | Observ("foo"), 174 | Observ("bar") 175 | ]) 176 | var changes = [] 177 | 178 | arr(function (state) { 179 | changes.push(state) 180 | }) 181 | 182 | arr.push(Observ("baz")) 183 | arr.splice(1, 1) 184 | 185 | assert.equal(changes.length, 2) 186 | assert.deepEqual(changes[0].slice(), ["foo", "bar", "baz"]) 187 | assert.deepEqual(changes[1].slice(), ["foo", "baz"]) 188 | 189 | assert.end() 190 | }) 191 | 192 | test("can add values to observ array & listen", function (assert) { 193 | var arr = ObservArray([]) 194 | var changes = [] 195 | 196 | arr(function (state) { 197 | changes.push(state) 198 | }) 199 | 200 | arr.push(Observ("foo")) 201 | arr.push(Observ("bar")) 202 | 203 | arr.get(0).set("foo2") 204 | arr.get(1).set("bar2") 205 | 206 | assert.equal(changes.length, 4) 207 | 208 | assert.deepEqual(changes[0].slice(), ["foo"]) 209 | assert.deepEqual(changes[1].slice(), ["foo", "bar"]) 210 | assert.deepEqual(changes[2].slice(), ["foo2", "bar"]) 211 | assert.deepEqual(changes[3].slice(), ["foo2", "bar2"]) 212 | 213 | assert.end() 214 | }) 215 | 216 | test("can remove values to observ & not blow up", function (assert) { 217 | var arr = ObservArray([ Observ("foo"), Observ("bar") ]) 218 | var changes = [] 219 | 220 | arr(function (state) { 221 | changes.push(state) 222 | }) 223 | 224 | var bar = arr.splice(1, 1)[0] 225 | 226 | assert.doesNotThrow(function () { 227 | bar.set("foobar") 228 | }) 229 | 230 | arr.push(Observ("baz")) 231 | 232 | var baz = arr.splice(1, 1)[0] 233 | 234 | assert.doesNotThrow(function () { 235 | baz.set("foobaz") 236 | }) 237 | 238 | assert.equal(changes.length, 3) 239 | assert.deepEqual(changes[0].slice(), ["foo"]) 240 | assert.deepEqual(changes[1].slice(), ["foo", "baz"]) 241 | assert.deepEqual(changes[2].slice(), ["foo"]) 242 | 243 | assert.end() 244 | }) 245 | 246 | test("can use put to override existing value", function (assert) { 247 | var arr = ObservArray([ Observ("foo"), Observ("bar") ]) 248 | var changes = [] 249 | 250 | arr(function (state) { 251 | changes.push(state) 252 | }) 253 | 254 | arr.put(0, Observ("baz")) 255 | arr.put(1, Observ("foobar")) 256 | 257 | assert.equal(changes.length, 2) 258 | assert.deepEqual(changes[0].slice(), ["baz", "bar"]) 259 | assert.deepEqual(changes[0]._diff, [ [0, 1, "baz"] ]) 260 | assert.deepEqual(changes[1].slice(), ["baz", "foobar"]) 261 | assert.deepEqual(changes[1]._diff, [ [1, 1, "foobar"] ]) 262 | 263 | assert.end() 264 | }) 265 | 266 | test("can put values into array beyond length", function (assert) { 267 | var arr = ObservArray([ Observ("foo"), Observ("bar") ]) 268 | var changes = [] 269 | 270 | arr(function (state) { 271 | changes.push(state) 272 | }) 273 | 274 | var baz = Observ("baz") 275 | arr.put(4, baz) 276 | 277 | baz.set("foobaz") 278 | 279 | assert.equal(changes.length, 2) 280 | assert.deepEqual(changes[0].slice(), ["foo", "bar", , , "baz"]) 281 | assert.deepEqual(changes[0]._diff, [ [4, 0, "baz"] ]) 282 | assert.deepEqual(changes[1].slice(), ["foo", "bar", , , "foobaz"]) 283 | assert.deepEqual(changes[1]._diff, [ [4, 1, "foobaz"] ]) 284 | 285 | assert.end() 286 | }) 287 | 288 | test("batch changes with transactions", function (assert) { 289 | 290 | var items = { 291 | foo: Observ("foo"), 292 | bar: Observ("bar"), 293 | foobar: Observ("foobar"), 294 | baz: Observ("baz"), 295 | bazbar: Observ("bazbar"), 296 | foobaz: Observ("foobaz"), 297 | foobarbaz: Observ("foobarbaz") 298 | } 299 | 300 | var arr = ObservArray([ items.foo, items.bar ]) 301 | var changes = [] 302 | 303 | arr(function (state) { 304 | changes.push(state) 305 | }) 306 | 307 | arr.transaction(function(rawList){ 308 | rawList.push(items.foobar) 309 | rawList.splice(1, 1, items.baz, items.bazbar) 310 | rawList.unshift(items.foobaz) 311 | rawList[6] = items.foobarbaz 312 | }) 313 | 314 | assert.equal(changes.length, 1) 315 | 316 | assert.deepEqual(changes[0].slice(), [ 317 | "foobaz","foo","baz","bazbar","foobar", undefined, "foobarbaz" 318 | ]) 319 | 320 | // check internal list 321 | assert.equal(arr._list.length, changes[0].length) 322 | changes[0].forEach(function(val, i){ 323 | assert.equal(arr._list[i], items[val]) 324 | }) 325 | 326 | assert.deepEqual(changes[0]._diff, [ 327 | [1,1,"baz","bazbar","foobar", undefined, "foobarbaz"], 328 | [0,0,"foobaz"] 329 | ]) 330 | 331 | assert.end() 332 | }) 333 | 334 | test("set updates array rather than replacing observ value", function (assert) { 335 | 336 | var items = { 337 | foo: Observ("foo"), 338 | bar: Observ("bar"), 339 | foobar: Observ("foobar"), 340 | baz: Observ("baz"), 341 | bazbar: Observ("bazbar") 342 | } 343 | 344 | var arr = ObservArray([ items.foo, items.bar, items.baz ]) 345 | var changes = [] 346 | 347 | arr(function (state) { 348 | changes.push(state) 349 | }) 350 | 351 | arr.set([ items.foo, items.foobar, items.baz, items.bazbar ]) 352 | 353 | assert.equal(changes.length, 1) 354 | 355 | assert.deepEqual(changes[0].slice(), [ 356 | "foo","foobar","baz","bazbar" 357 | ]) 358 | 359 | assert.deepEqual(changes[0]._diff, [ 360 | [1,1,"foobar"], 361 | [3,0,"bazbar"] 362 | ]) 363 | 364 | assert.end() 365 | }) 366 | 367 | require('./reverse.js') 368 | require('./sort.js') 369 | -------------------------------------------------------------------------------- /test/reverse.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var Observ = require("observ") 3 | var computed = require("observ/computed") 4 | var Struct = require('observ-struct') 5 | var ObservArray = require("../index") 6 | 7 | test("ObservArray array.reverse should work", function (assert) { 8 | var arr = ObservArray([ 9 | Observ(1), 10 | Observ(2), 11 | Observ(3) 12 | ]) 13 | 14 | var changes = [] 15 | var initial = arr() 16 | 17 | arr(function(state) { 18 | changes.push(state) 19 | }) 20 | 21 | arr.reverse() 22 | 23 | var reversed = arr() 24 | 25 | assert.equal(changes.length, 1) 26 | assert.deepEqual(changes[0], [3, 2, 1]) 27 | assert.deepEqual(changes[0]._diff, [ [0, 1, 3], [2, 1, 1] ]) 28 | 29 | assert.end() 30 | }) 31 | 32 | test("still emits changes after reversal", function (assert) { 33 | var arr = ObservArray([ 34 | Observ("foo"), 35 | Observ("bar"), 36 | Observ("baz") 37 | ]) 38 | 39 | var changes = [] 40 | 41 | arr(function(state) { 42 | changes.push(state) 43 | }) 44 | 45 | arr.reverse() 46 | arr.get(0).set("baz2") 47 | arr.get(1).set("bar2") 48 | 49 | assert.equal(changes.length, 3) 50 | assert.deepEqual(changes[1], [ "baz2", "bar", "foo" ]) 51 | assert.deepEqual(changes[2], [ "baz2", "bar2", "foo" ]) 52 | 53 | assert.end() 54 | }) 55 | 56 | test("reverse works with nested arrays", function (assert) { 57 | var arr = ObservArray([ 58 | Observ("foo"), 59 | ObservArray([ 60 | Observ("bar"), 61 | Observ("baz") 62 | ]) 63 | ]) 64 | var initArr = arr() 65 | var changes = [] 66 | var innerChanges = [] 67 | 68 | arr(function (state) { 69 | changes.push(state) 70 | }) 71 | 72 | arr.get(1)(function (state) { 73 | innerChanges.push(state) 74 | }) 75 | 76 | arr.reverse() 77 | 78 | arr.get(0).get(0).set("bar2") 79 | arr.get(0).get(1).set("baz2") 80 | arr.get(1).set("foo2") 81 | 82 | assert.equal(changes.length, 4) 83 | assert.equal(innerChanges.length, 2) 84 | 85 | assert.deepEqual(changes[0], [ 86 | [ "bar", "baz" ], 87 | "foo" 88 | ]) 89 | 90 | assert.notEqual(changes[0], initArr) 91 | assert.notEqual(changes[1], changes[0]) 92 | assert.notEqual(changes[2], changes[1]) 93 | assert.notEqual(changes[3], changes[2]) 94 | 95 | assert.notEqual(innerChanges[0], initArr[1]) 96 | assert.notEqual(innerChanges[1], innerChanges[0]) 97 | 98 | assert.deepEqual(initArr, [ 99 | "foo", 100 | ["bar", "baz"] 101 | ]) 102 | 103 | assert.deepEqual(changes[1][0].slice(), ["bar2", "baz"]) 104 | assert.equal(changes[1][1], "foo") 105 | 106 | assert.deepEqual(changes[2][0].slice(), ["bar2", "baz2"]) 107 | assert.equal(changes[2][1], "foo") 108 | 109 | assert.equal(changes[3][1], "foo2") 110 | 111 | assert.deepEqual(initArr[1], ["bar", "baz"]) 112 | assert.deepEqual(innerChanges[0].slice(), ["bar2", "baz"]) 113 | assert.deepEqual(innerChanges[1].slice(), ["bar2", "baz2"]) 114 | 115 | assert.equal(changes[0][1], changes[1][1], 116 | "unchanged properties are the same value") 117 | 118 | assert.end() 119 | }) 120 | 121 | test("can call array methods on value inside reversed array", function (assert) { 122 | var arr = ObservArray([ Observ("foo"), Observ("bar") ]) 123 | 124 | arr.reverse() 125 | 126 | var v = arr() 127 | 128 | var list = v.slice() 129 | var doubles = list.map(function (v) { 130 | return v + v 131 | }) 132 | 133 | assert.ok(Array.isArray(v)) 134 | assert.deepEqual(doubles, [ "barbar", "foofoo" ]) 135 | 136 | assert.end() 137 | }) 138 | 139 | test("can call array methods on reversed ObservArray", function (assert) { 140 | var arr = ObservArray([ 141 | Observ(0), 142 | Observ(1), 143 | Observ(2), 144 | Observ(3), 145 | Observ(5) 146 | ]) 147 | 148 | arr.reverse() 149 | 150 | var doubles = arr.map(function (o) { 151 | return computed([o], function (o) { return o * 2 }) 152 | }) 153 | 154 | var changes = [] 155 | 156 | doubles(function (state) { 157 | changes.push(state) 158 | }) 159 | 160 | assert.equal(typeof doubles.get, "function") 161 | assert.equal(typeof doubles.getLength, "function") 162 | assert.equal(Array.isArray(doubles), false) 163 | 164 | arr.get(2).set(5) 165 | 166 | assert.equal(changes.length, 1) 167 | assert.deepEqual(changes[0].slice(), [ 168 | 10, 6, 10, 2, 0 169 | ]) 170 | 171 | doubles.push(Observ(8)) 172 | 173 | assert.equal(changes.length, 2) 174 | assert.deepEqual(changes[1].slice(), [ 175 | 10, 6, 10, 2, 0, 8 176 | ]) 177 | 178 | assert.end() 179 | }) 180 | 181 | test("can add values to reversed observ array", function (assert) { 182 | var arr = ObservArray([ 183 | Observ("foo"), 184 | Observ("bar") 185 | ]) 186 | 187 | arr.reverse() 188 | 189 | var changes = [] 190 | 191 | arr(function (state) { 192 | changes.push(state) 193 | }) 194 | 195 | arr.push(Observ("baz")) 196 | arr.splice(1, 1) 197 | 198 | assert.equal(changes.length, 2) 199 | assert.deepEqual(changes[0].slice(), ["bar", "foo", "baz"]) 200 | assert.deepEqual(changes[1].slice(), ["bar", "baz"]) 201 | 202 | assert.end() 203 | }) 204 | 205 | test("can add values to reversed observ array & listen", function (assert) { 206 | var arr = ObservArray([]) 207 | var changes = [] 208 | 209 | arr.reverse() 210 | 211 | arr(function (state) { 212 | changes.push(state) 213 | }) 214 | 215 | arr.push(Observ("foo")) 216 | arr.push(Observ("bar")) 217 | 218 | arr.get(0).set("foo2") 219 | arr.get(1).set("bar2") 220 | 221 | assert.equal(changes.length, 4) 222 | 223 | assert.deepEqual(changes[0].slice(), ["foo"]) 224 | assert.deepEqual(changes[1].slice(), ["foo", "bar"]) 225 | assert.deepEqual(changes[2].slice(), ["foo2", "bar"]) 226 | assert.deepEqual(changes[3].slice(), ["foo2", "bar2"]) 227 | 228 | assert.end() 229 | }) 230 | 231 | test("can use put to override existing value in reversed array", function (assert) { 232 | var arr = ObservArray([ Observ("foo"), Observ("bar") ]) 233 | var changes = [] 234 | 235 | arr.reverse() 236 | 237 | arr(function (state) { 238 | changes.push(state) 239 | }) 240 | 241 | arr.put(0, Observ("baz")) 242 | arr.put(1, Observ("foobar")) 243 | 244 | assert.equal(changes.length, 2) 245 | assert.deepEqual(changes[0].slice(), ["baz", "foo"]) 246 | assert.deepEqual(changes[0]._diff, [ [0, 1, "baz"] ]) 247 | assert.deepEqual(changes[1].slice(), ["baz", "foobar"]) 248 | assert.deepEqual(changes[1]._diff, [ [1, 1, "foobar"] ]) 249 | 250 | assert.end() 251 | }) 252 | 253 | test("can put values into reversed array beyond length", function (assert) { 254 | var arr = ObservArray([ Observ("foo"), Observ("bar") ]) 255 | var changes = [] 256 | 257 | arr.reverse() 258 | 259 | arr(function (state) { 260 | changes.push(state) 261 | }) 262 | 263 | var baz = Observ("baz") 264 | arr.put(4, baz) 265 | 266 | baz.set("foobaz") 267 | 268 | assert.equal(changes.length, 2) 269 | assert.deepEqual(changes[0].slice(), ["bar", "foo", , , "baz"]) 270 | assert.deepEqual(changes[0]._diff, [ [4, 0, "baz"] ]) 271 | assert.deepEqual(changes[1].slice(), ["bar", "foo", , , "foobaz"]) 272 | assert.deepEqual(changes[1]._diff, [ [4, 1, "foobaz"] ]) 273 | 274 | assert.end() 275 | }) 276 | 277 | test("batch changes with transactions on reversed array", function (assert) { 278 | 279 | var items = { 280 | foo: Observ("foo"), 281 | bar: Observ("bar"), 282 | foobar: Observ("foobar"), 283 | baz: Observ("baz"), 284 | bazbar: Observ("bazbar"), 285 | foobaz: Observ("foobaz"), 286 | foobarbaz: Observ("foobarbaz") 287 | } 288 | 289 | var arr = ObservArray([ items.foo, items.bar ]) 290 | var changes = [] 291 | 292 | arr.reverse() 293 | 294 | arr(function (state) { 295 | changes.push(state) 296 | }) 297 | 298 | arr.transaction(function(rawList){ 299 | rawList.push(items.foobar) 300 | rawList.splice(1, 1, items.baz, items.bazbar) 301 | rawList.unshift(items.foobaz) 302 | rawList[6] = items.foobarbaz 303 | }) 304 | 305 | assert.equal(changes.length, 1) 306 | 307 | assert.deepEqual(changes[0].slice(), [ 308 | "foobaz","bar","baz","bazbar","foobar", undefined, "foobarbaz" 309 | ]) 310 | 311 | // check internal list 312 | assert.equal(arr._list.length, changes[0].length) 313 | changes[0].forEach(function(val, i){ 314 | assert.equal(arr._list[i], items[val]) 315 | }) 316 | 317 | assert.deepEqual(changes[0]._diff, [ 318 | [1,1,"baz","bazbar","foobar", undefined, "foobarbaz"], 319 | [0,0,"foobaz"] 320 | ]) 321 | 322 | assert.end() 323 | }) 324 | 325 | test("set updates reversed array rather than replacing observ value", function (assert) { 326 | 327 | var items = { 328 | foo: Observ("foo"), 329 | bar: Observ("bar"), 330 | foobar: Observ("foobar"), 331 | baz: Observ("baz"), 332 | bazbar: Observ("bazbar") 333 | } 334 | 335 | var arr = ObservArray([ items.foo, items.bar, items.baz ]) 336 | var changes = [] 337 | 338 | arr.reverse() 339 | 340 | arr(function (state) { 341 | changes.push(state) 342 | }) 343 | 344 | assert.deepEqual(arr(), [ "baz", "bar", "foo" ]) 345 | 346 | arr.set([ items.foo, items.foobar, items.baz, items.bazbar ]) 347 | 348 | arr.reverse() 349 | 350 | assert.equal(changes.length, 2) 351 | 352 | assert.deepEqual(changes[0].slice(), [ 353 | "foo","foobar","baz","bazbar" 354 | ]) 355 | 356 | assert.deepEqual(changes[0]._diff, [ 357 | [0, 3, "foo","foobar","baz","bazbar"] 358 | ]) 359 | 360 | assert.deepEqual(changes[1].slice(), [ 361 | "bazbar", "baz", "foobar", "foo" 362 | ]) 363 | 364 | assert.deepEqual(changes[1]._diff, [ 365 | [0, 4, "bazbar", "baz", "foobar", "foo" ] 366 | ]) 367 | 368 | assert.end() 369 | }) 370 | 371 | test("reverse preserves observables after reversing", function(assert) { 372 | var items = { 373 | foo: Observ("foo"), 374 | bar: Observ("foo"), 375 | foobar: Observ("foo"), 376 | baz: Observ("foo"), 377 | bazbar: Observ("foo") 378 | } 379 | 380 | var arr = ObservArray([ items.foo, items.bar, items.foobar ]) 381 | 382 | var changes = [] 383 | 384 | arr(function(state) { 385 | changes.push(state) 386 | }) 387 | 388 | arr.reverse() 389 | 390 | items.foo.set("foo2") 391 | items.bar.set("bar") 392 | items.foobar.set("foobar") 393 | 394 | assert.equal(changes.length, 4) 395 | 396 | assert.deepEqual(changes[0].slice(), [ 397 | "foo", "foo", "foo" 398 | ]) 399 | assert.deepEqual(changes[0]._diff, [ 400 | [ 0, 1, "foo" ], 401 | [ 2, 1, "foo" ] 402 | ]) 403 | 404 | assert.deepEqual(changes[1], [ 405 | "foo", "foo", "foo2" 406 | ]) 407 | assert.deepEqual(changes[1]._diff, [ 408 | [ 2, 1, "foo2" ] 409 | ]) 410 | 411 | assert.deepEqual(changes[2], [ 412 | "foo", "bar", "foo2" 413 | ]) 414 | assert.deepEqual(changes[2]._diff, [ 415 | [ 1, 1, "bar" ] 416 | ]) 417 | 418 | assert.deepEqual(changes[3], [ 419 | "foobar", "bar", "foo2" 420 | ]) 421 | assert.deepEqual(changes[3]._diff, [ 422 | [ 0, 1, "foobar" ] 423 | ]) 424 | 425 | assert.end() 426 | }) 427 | -------------------------------------------------------------------------------- /test/sort.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var Observ = require("observ") 3 | var computed = require("observ/computed") 4 | var ObservStruct = require('observ-struct') 5 | var ObservVarHash = require("observ-varhash") 6 | var ObservArray = require("../index") 7 | 8 | test("ObservArray.sort() should work", function (assert) { 9 | var arr = ObservArray([ 10 | Observ(3), 11 | Observ(2), 12 | Observ(1), 13 | Observ(0) 14 | ]) 15 | 16 | var changes = [] 17 | 18 | arr(function(state) { 19 | changes.push(state) 20 | }) 21 | 22 | arr.sort() 23 | 24 | assert.equal(changes.length, 1) 25 | assert.deepEqual(changes[0].slice(), [ 0, 1, 2, 3 ]) 26 | assert.deepEqual(changes[0]._diff, [ [ 0, 4, 0, 1, 2, 3 ] ]) 27 | 28 | assert.end() 29 | }) 30 | 31 | test("ObservArray.sort(cmp_fn) should work", function (assert) { 32 | var arr = ObservArray([ 33 | Observ(0), 34 | Observ(1), 35 | Observ(2), 36 | Observ(3) 37 | ]) 38 | 39 | var changes = [] 40 | 41 | arr(function(state) { 42 | changes.push(state) 43 | }) 44 | 45 | arr.sort(function(a, b) { 46 | return a < b 47 | }) 48 | 49 | assert.equal(changes.length, 1) 50 | assert.deepEqual(changes[0].slice(), [ 3, 2, 1, 0 ]) 51 | assert.deepEqual(changes[0]._diff, [ [ 0, 4, 3, 2, 1, 0 ] ]) 52 | 53 | assert.end() 54 | }) 55 | 56 | test("ObservArray.sort raw values", function (assert) { 57 | var arr = ObservArray([ 58 | 0, 1, 2, 3 59 | ]) 60 | 61 | var changes = [] 62 | 63 | arr(function(state) { 64 | changes.push(state) 65 | }) 66 | 67 | arr.sort(function(a, b) { 68 | return a < b 69 | }) 70 | 71 | assert.equal(changes.length, 1) 72 | assert.deepEqual(changes[0].slice(), [ 3, 2, 1, 0 ]) 73 | assert.deepEqual(changes[0]._diff, [ [ 0, 4, 3, 2, 1, 0 ] ]) 74 | 75 | assert.end() 76 | }) 77 | 78 | test("can sort by struct key", function(assert) { 79 | var arr = ObservArray([ 80 | ObservStruct({ foo: "a" }), 81 | ObservStruct({ foo: "b" }), 82 | ObservStruct({ foo: "c" }) 83 | ]) 84 | 85 | var changes = [] 86 | 87 | arr(function(state) { 88 | changes.push(state) 89 | }) 90 | 91 | arr.sort(function(a, b) { 92 | return a.foo < b.foo 93 | }) 94 | 95 | assert.equal(changes.length, 1) 96 | 97 | assert.deepEqual(changes[0].slice(), [ 98 | { foo: "c" }, 99 | { foo: "b" }, 100 | { foo: "a" } 101 | ]) 102 | 103 | assert.deepEqual(changes[0]._diff, [ 104 | [ 0, 3, 105 | { foo: "c" }, 106 | { foo: "b" }, 107 | { foo: "a" } 108 | ] 109 | ]) 110 | 111 | assert.end() 112 | }) 113 | 114 | test("ObservArray sort should preserve Observ", function(assert) { 115 | var obsA = Observ("A") 116 | var obsB = Observ("B") 117 | var obsC = Observ("C") 118 | 119 | var arr = ObservArray([ 120 | obsA, obsB, obsC 121 | ]) 122 | 123 | var changes = [] 124 | 125 | arr(function(state) { 126 | changes.push(state) 127 | }) 128 | 129 | arr.sort(function(a, b) { 130 | return a < b 131 | }) 132 | 133 | obsA.set("A2") 134 | 135 | assert.equal(changes.length, 2) 136 | 137 | assert.deepEqual(changes[0].slice(), [ 138 | "C", "B", "A" 139 | ]) 140 | 141 | assert.deepEqual(changes[0]._diff, [ 142 | [ 0, 3, "C", "B", "A" ] 143 | ]) 144 | 145 | assert.deepEqual(changes[1].slice(), [ 146 | "C", "B", "A2" 147 | ]) 148 | 149 | assert.deepEqual(changes[1]._diff, [ 150 | [ 2, 1, "A2" ] 151 | ]) 152 | 153 | assert.end() 154 | }) 155 | 156 | test("ObservArray sort should preserve vvarhaslt harhash objects", function(assert) { 157 | var vhA = ObservVarHash({}) 158 | var vhB = ObservVarHash({}) 159 | var vhC = ObservVarHash({}) 160 | 161 | var arr = ObservArray([ 162 | vhA, vhB, vhC 163 | ]) 164 | 165 | var changes = [] 166 | 167 | arr(function(state) { 168 | changes.push(state) 169 | }) 170 | 171 | vhA.put("foo", "A") 172 | vhB.put("foo", "B") 173 | vhC.put("foo", "C") 174 | 175 | arr.sort(function(a, b) { 176 | return a.foo < b.foo 177 | }) 178 | 179 | arr.get(0).put("foo", "C2") 180 | 181 | vhA.delete("foo") 182 | 183 | assert.equal(changes.length, 6) 184 | 185 | assert.deepEqual(changes[0].slice(), [ 186 | { foo: "A" }, {}, {} 187 | ]) 188 | assert.deepEqual(changes[0]._diff, [ 189 | [0, 1, { foo: "A" }] 190 | ]) 191 | assert.deepEqual(changes[1].slice(), [ 192 | { foo: "A" }, { foo: "B" }, { } 193 | ]) 194 | assert.deepEqual(changes[1]._diff, [ 195 | [1, 1, { foo: "B" }] 196 | ]) 197 | assert.deepEqual(changes[2].slice(), [ 198 | { foo: "A" }, { foo: "B" }, { foo: "C" } 199 | ]) 200 | assert.deepEqual(changes[2]._diff, [ 201 | [2, 1, { foo: "C" }] 202 | ]) 203 | 204 | assert.deepEqual(changes[3].slice(), [ 205 | { foo: "C" }, { foo: "B" }, { foo: "A" } 206 | ]) 207 | assert.deepEqual(changes[3]._diff, [ 208 | [0, 3, { foo: "C" }, { foo: "B" }, { foo: "A" }] 209 | ]) 210 | 211 | assert.deepEqual(changes[4].slice(), [ 212 | { foo: "C2" }, { foo: "B" }, { foo: "A" } 213 | ]) 214 | 215 | assert.deepEqual(changes[4]._diff, [ 216 | [ 0, 1, { foo: "C2" } ] 217 | ]) 218 | 219 | assert.deepEqual(changes[5].slice(), [ 220 | { foo: "C2" }, { foo: "B" }, { } 221 | ]) 222 | 223 | assert.deepEqual(changes[5]._diff, [ 224 | [ 2, 1, {} ] 225 | ]) 226 | assert.end() 227 | }) 228 | 229 | test("batch changes with transactions on sorted array", function (assert) { 230 | var items = { 231 | foo: Observ("foo"), 232 | bar: Observ("bar"), 233 | foobar: Observ("foobar"), 234 | baz: Observ("baz"), 235 | bazbar: Observ("bazbar"), 236 | foobaz: Observ("foobaz"), 237 | foobarbaz: Observ("foobarbaz") 238 | } 239 | 240 | var arr = ObservArray([ items.foo, items.bar ]) 241 | var changes = [] 242 | 243 | arr(function (state) { 244 | changes.push(state) 245 | }) 246 | 247 | arr.sort() 248 | 249 | arr.transaction(function(rawList){ 250 | rawList.push(items.foobar) 251 | rawList.splice(1, 1, items.baz, items.bazbar) 252 | rawList.unshift(items.foobaz) 253 | rawList[6] = items.foobarbaz 254 | }) 255 | 256 | arr.sort() 257 | 258 | assert.equal(changes.length, 3) 259 | 260 | assert.deepEqual(changes[0].slice(), [ 261 | "bar", "foo" 262 | ]) 263 | 264 | assert.deepEqual(changes[0]._diff, [ 265 | [ 0, 2, "bar", "foo" ] 266 | ]) 267 | 268 | assert.deepEqual(changes[1].slice(), [ 269 | "foobaz", "bar", "baz", "bazbar", "foobar", undefined, "foobarbaz" 270 | ]) 271 | 272 | assert.deepEqual(changes[1]._diff, [ 273 | [1,1,"baz","bazbar","foobar", undefined, "foobarbaz"], 274 | [0,0,"foobaz"] 275 | ]) 276 | 277 | assert.deepEqual(changes[2].slice(), [ 278 | "bar", "baz", "bazbar", "foobar", "foobarbaz", "foobaz", undefined 279 | ]) 280 | assert.deepEqual(changes[2]._diff, [ 281 | [ 0, 7, "bar", "baz", "bazbar", "foobar", "foobarbaz", "foobaz", undefined ] 282 | ]) 283 | 284 | // check internal list 285 | assert.equal(arr._list.length, changes[2].length) 286 | 287 | changes[2].forEach(function(val, i){ 288 | assert.equal(arr._list[i], items[val]) 289 | }) 290 | 291 | assert.end() 292 | }) 293 | 294 | test("can call array methods on sorted ObservArray", function (assert) { 295 | var arr = ObservArray([ 296 | Observ(0), 297 | Observ(1), 298 | Observ(2), 299 | Observ(3), 300 | Observ(5) 301 | ]) 302 | 303 | arr.sort(function(a, b) { 304 | return a < b 305 | }) 306 | 307 | var doubles = arr.map(function (o) { 308 | return computed([o], function (o) { return o * 2 }) 309 | }) 310 | 311 | var changes = [] 312 | 313 | doubles(function (state) { 314 | changes.push(state) 315 | }) 316 | 317 | assert.equal(typeof doubles.get, "function") 318 | assert.equal(typeof doubles.getLength, "function") 319 | assert.equal(Array.isArray(doubles), false) 320 | 321 | arr.get(2).set(5) 322 | 323 | assert.equal(changes.length, 1) 324 | assert.deepEqual(changes[0].slice(), [ 325 | 10, 6, 10, 2, 0 326 | ]) 327 | 328 | doubles.push(Observ(8)) 329 | 330 | assert.equal(changes.length, 2) 331 | assert.deepEqual(changes[1].slice(), [ 332 | 10, 6, 10, 2, 0, 8 333 | ]) 334 | 335 | assert.end() 336 | }) 337 | 338 | test("sort should maintain observ objects of the same value", function(assert) { 339 | var a1 = Observ("A") 340 | var a2 = Observ("A") 341 | var b1 = Observ("B") 342 | 343 | var arr = ObservArray([ a1, b1, a2 ]) 344 | 345 | var changes = [] 346 | 347 | arr(function(state) { 348 | changes.push(state) 349 | }) 350 | 351 | arr.sort() 352 | 353 | a1.set("A1") 354 | a2.set("A2") 355 | 356 | assert.equal(changes.length, 3) 357 | 358 | assert.deepEqual(changes[0].slice(), [ "A", "A", "B" ]) 359 | assert.deepEqual(changes[0]._diff, [ 360 | [ 0, 3, "A", "A", "B" ] 361 | ]) 362 | 363 | assert.deepEqual(changes[1].slice(), [ "A1", "A", "B" ]) 364 | assert.deepEqual(changes[1]._diff, [ 365 | [ 0, 1, "A1" ] 366 | ]) 367 | 368 | assert.deepEqual(changes[2].slice(), [ "A1", "A2", "B" ]) 369 | assert.deepEqual(changes[2]._diff, [ 370 | [ 1, 1, "A2" ] 371 | ]) 372 | 373 | assert.end() 374 | }) 375 | -------------------------------------------------------------------------------- /transaction.js: -------------------------------------------------------------------------------- 1 | module.exports = transaction 2 | 3 | function transaction (func) { 4 | var obs = this 5 | var rawList = obs._list.slice() 6 | 7 | if (func(rawList) !== false){ // allow cancel 8 | return obs.set(rawList) 9 | } 10 | 11 | } --------------------------------------------------------------------------------