├── docs.mli ├── .travis.yml ├── .gitignore ├── .jshintrc ├── LICENCE ├── package.json ├── README.md ├── index.js └── test └── index.js /docs.mli: -------------------------------------------------------------------------------- 1 | 2 | observ-struct := (Object>) => 3 | Observ> & Object> 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "observ-struct", 3 | "version": "6.0.0", 4 | "description": "An object with observable key value pairs", 5 | "keywords": [], 6 | "author": "Raynos ", 7 | "repository": "git://github.com/Raynos/observ-struct.git", 8 | "main": "index", 9 | "homepage": "https://github.com/Raynos/observ-struct", 10 | "contributors": [ 11 | { 12 | "name": "Raynos" 13 | } 14 | ], 15 | "bugs": { 16 | "url": "https://github.com/Raynos/observ-struct/issues", 17 | "email": "raynos2@gmail.com" 18 | }, 19 | "dependencies": { 20 | "observ": "~0.2.0", 21 | "xtend": "^4.0.0" 22 | }, 23 | "devDependencies": { 24 | "istanbul": "^0.3.13", 25 | "tape": "~2.5.0" 26 | }, 27 | "licenses": [ 28 | { 29 | "type": "MIT", 30 | "url": "http://github.com/Raynos/observ-struct/raw/master/LICENSE" 31 | } 32 | ], 33 | "scripts": { 34 | "test": "node ./test/index.js", 35 | "start": "node ./index.js", 36 | "watch": "nodemon -w ./index.js index.js", 37 | "travis-test": "istanbul cover ./test/index.js && ((cat coverage/lcov.info | coveralls) || exit 0)", 38 | "cover": "istanbul cover --report none --print detail ./test/index.js", 39 | "view-cover": "istanbul report html && google-chrome ./coverage/index.html", 40 | "test-browser": "testem-browser ./test/browser/index.js", 41 | "testem": "testem-both -b=./test/browser/index.js" 42 | }, 43 | "testling": { 44 | "files": "test/index.js", 45 | "browsers": [ 46 | "ie/8..latest", 47 | "firefox/16..latest", 48 | "firefox/nightly", 49 | "chrome/22..latest", 50 | "chrome/canary", 51 | "opera/12..latest", 52 | "opera/next", 53 | "safari/5.1..latest", 54 | "ipad/6.0..latest", 55 | "iphone/6.0..latest", 56 | "android-browser/4.2..latest" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # observ-struct 2 | 3 | 10 | 11 | 12 | 13 | An object with observable key value pairs 14 | 15 | ## Example 16 | 17 | An observable will emit a new immutable value whenever one of 18 | its keys changes. 19 | 20 | Nested keys will still be the same value if they were not changed 21 | in that particular `.set()` call. 22 | 23 | ```js 24 | var ObservStruct = require("observ-struct") 25 | var Observ = require("observ") 26 | var assert = require("assert") 27 | 28 | var state = ObservStruct({ 29 | fruits: ObservStruct({ 30 | apples: Observ(3), 31 | oranges: Observ(5) 32 | }), 33 | customers: Observ(5) 34 | }) 35 | 36 | state(function (current) { 37 | console.log("apples", current.fruits.apples) 38 | console.log("customers", current.customers) 39 | }) 40 | 41 | state.fruits(function (current) { 42 | console.log("apples", current.apples) 43 | }) 44 | 45 | var initialState = state() 46 | assert.equal(initialState.fruits.oranges, 5) 47 | assert.equal(initialState.customers, 5) 48 | 49 | state.fruits.oranges.set(6) 50 | state.customers.set(5) 51 | state.fruits.apples.set(4) 52 | ``` 53 | 54 | ## Docs 55 | 56 | ### `var obj = ObservStruct(opts)` 57 | 58 | `ObservStruct()` takes an object literal of string keys to either 59 | normal values or observable values. 60 | 61 | It returns an `Observ` instance `obj`. The value of `obj` is 62 | a plain javascript object where the value for each key is either 63 | the normal value passed in or the value of the observable for 64 | that key. 65 | 66 | Whenever one of the observables on a `key` changes the `obj` will 67 | emit a new object that's a shallow copy with that `key` set to 68 | the value of the appropiate observable on that `key`. 69 | 70 | ## Installation 71 | 72 | `npm install observ-struct` 73 | 74 | ## Contributors 75 | 76 | - Raynos 77 | 78 | ## MIT Licenced 79 | 80 | [1]: https://secure.travis-ci.org/Raynos/observ-struct.png 81 | [2]: https://travis-ci.org/Raynos/observ-struct 82 | [3]: https://badge.fury.io/js/observ-struct.png 83 | [4]: https://badge.fury.io/js/observ-struct 84 | [5]: https://coveralls.io/repos/Raynos/observ-struct/badge.png 85 | [6]: https://coveralls.io/r/Raynos/observ-struct 86 | [7]: https://gemnasium.com/Raynos/observ-struct.png 87 | [8]: https://gemnasium.com/Raynos/observ-struct 88 | [9]: https://david-dm.org/Raynos/observ-struct.png 89 | [10]: https://david-dm.org/Raynos/observ-struct 90 | [11]: https://ci.testling.com/Raynos/observ-struct.png 91 | [12]: https://ci.testling.com/Raynos/observ-struct 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Observ = require("observ") 2 | var extend = require("xtend") 3 | 4 | var blackList = { 5 | "length": "Clashes with `Function.prototype.length`.\n", 6 | "name": "Clashes with `Function.prototype.name`.\n", 7 | "_diff": "_diff is reserved key of observ-struct.\n", 8 | "_type": "_type is reserved key of observ-struct.\n", 9 | "_version": "_version is reserved key of observ-struct.\n" 10 | } 11 | var NO_TRANSACTION = {} 12 | 13 | function setNonEnumerable(object, key, value) { 14 | Object.defineProperty(object, key, { 15 | value: value, 16 | writable: true, 17 | configurable: true, 18 | enumerable: false 19 | }) 20 | } 21 | 22 | /* ObservStruct := (Object>) => 23 | Object> & 24 | Observ & { 25 | _diff: Object 26 | }> 27 | 28 | */ 29 | module.exports = ObservStruct 30 | 31 | function ObservStruct(struct) { 32 | var keys = Object.keys(struct) 33 | 34 | var initialState = {} 35 | var currentTransaction = NO_TRANSACTION 36 | var nestedTransaction = NO_TRANSACTION 37 | 38 | keys.forEach(function (key) { 39 | if (blackList.hasOwnProperty(key)) { 40 | throw new Error("cannot create an observ-struct " + 41 | "with a key named '" + key + "'.\n" + 42 | blackList[key]); 43 | } 44 | 45 | var observ = struct[key] 46 | initialState[key] = typeof observ === "function" ? 47 | observ() : observ 48 | }) 49 | 50 | var obs = Observ(initialState) 51 | keys.forEach(function (key) { 52 | var observ = struct[key] 53 | obs[key] = observ 54 | 55 | if (typeof observ === "function") { 56 | observ(function (value) { 57 | if (nestedTransaction === value) { 58 | return 59 | } 60 | 61 | var state = extend(obs()) 62 | state[key] = value 63 | var diff = {} 64 | diff[key] = value && value._diff ? 65 | value._diff : value 66 | 67 | setNonEnumerable(state, "_diff", diff) 68 | currentTransaction = state 69 | obs.set(state) 70 | currentTransaction = NO_TRANSACTION 71 | }) 72 | } 73 | }) 74 | var _set = obs.set 75 | obs.set = function trackDiff(value) { 76 | if (currentTransaction === value) { 77 | return _set(value) 78 | } 79 | 80 | var newState = extend(value) 81 | setNonEnumerable(newState, "_diff", value) 82 | _set(newState) 83 | } 84 | 85 | obs(function (newState) { 86 | if (currentTransaction === newState) { 87 | return 88 | } 89 | 90 | keys.forEach(function (key) { 91 | var observ = struct[key] 92 | var newObservValue = newState[key] 93 | 94 | if (typeof observ === "function" && 95 | observ() !== newObservValue 96 | ) { 97 | nestedTransaction = newObservValue 98 | observ.set(newState[key]) 99 | nestedTransaction = NO_TRANSACTION 100 | } 101 | }) 102 | }) 103 | 104 | obs._type = "observ-struct" 105 | obs._version = "5" 106 | 107 | return obs 108 | } 109 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require("tape") 2 | var Observ = require("observ") 3 | 4 | var ObservHash = require("../index") 5 | 6 | test("ObservHash is a function", function (assert) { 7 | assert.equal(typeof ObservHash, "function") 8 | assert.end() 9 | }) 10 | 11 | test("observ contains correct initial value", function (assert) { 12 | var obj = ObservHash({ 13 | foo: Observ("foo"), 14 | bar: Observ("bar") 15 | }) 16 | 17 | var state = obj() 18 | assert.equal(state.foo, "foo") 19 | assert.equal(state.bar, "bar") 20 | 21 | assert.end() 22 | }) 23 | 24 | test("observ emits change", function (assert) { 25 | var obj = ObservHash({ 26 | foo: Observ("foo"), 27 | bar: Observ("bar") 28 | }) 29 | var changes = [] 30 | 31 | obj(function (state) { 32 | changes.push(state) 33 | }) 34 | 35 | obj.foo.set("foo") 36 | obj.foo.set("foo2") 37 | obj.bar.set("bar2") 38 | 39 | assert.equal(changes.length, 3) 40 | assert.deepEqual(changes[0], { 41 | foo: "foo", bar: "bar", 42 | }) 43 | assert.deepEqual(changes[0]._diff, { "foo": "foo" }) 44 | assert.deepEqual(changes[1], { 45 | foo: "foo2", bar: "bar" 46 | }) 47 | assert.deepEqual(changes[1]._diff, { "foo": "foo2" }) 48 | assert.deepEqual(changes[2], { 49 | foo: "foo2", bar: "bar2" 50 | }) 51 | assert.deepEqual(changes[2]._diff, { "bar": "bar2" }) 52 | assert.notEqual(changes[0], changes[1]) 53 | assert.notEqual(changes[1], changes[2]) 54 | 55 | assert.end() 56 | }) 57 | 58 | test("supports both observs and values", function (assert) { 59 | var obj = ObservHash({ 60 | foo: Observ("foo"), 61 | bar: "bar" 62 | }) 63 | 64 | assert.equal(typeof obj.foo, "function") 65 | assert.equal(obj.foo(), "foo") 66 | assert.equal(obj.bar, "bar") 67 | 68 | assert.end() 69 | }) 70 | 71 | test("works with nested things", function (assert) { 72 | var obj = ObservHash({ 73 | fruits: ObservHash({ 74 | apples: Observ(3), 75 | oranges: Observ(5) 76 | }), 77 | customers: Observ(5) 78 | }) 79 | var initialState = obj() 80 | var changes = [] 81 | var fruitChanges = [] 82 | 83 | obj(function (state) { 84 | changes.push(state) 85 | }) 86 | 87 | obj.fruits(function (state) { 88 | fruitChanges.push(state) 89 | }) 90 | 91 | obj.fruits.oranges.set(6) 92 | obj.customers.set(10) 93 | obj.fruits.apples.set(4) 94 | 95 | assert.equal(changes.length, 3) 96 | assert.equal(fruitChanges.length, 2) 97 | 98 | assert.notEqual(changes[0], initialState) 99 | assert.notEqual(changes[1], changes[0]) 100 | assert.notEqual(changes[2], changes[1]) 101 | 102 | assert.notEqual(fruitChanges[0], initialState.fruits) 103 | assert.notEqual(fruitChanges[1], fruitChanges[0]) 104 | 105 | assert.deepEqual(initialState, { 106 | customers: 5, 107 | fruits: { apples: 3, oranges: 5 } 108 | }) 109 | 110 | assert.deepEqual(changes[0], { 111 | customers: 5, 112 | fruits: { apples: 3, oranges: 6 } 113 | }) 114 | assert.deepEqual(changes[0]._diff, { fruits: { oranges: 6 } }) 115 | assert.deepEqual(changes[0].fruits._diff, { "oranges": 6 }) 116 | assert.deepEqual(changes[1], { 117 | customers: 10, 118 | fruits: { apples: 3, oranges: 6 } 119 | }) 120 | assert.deepEqual(changes[1]._diff, { customers: 10 }) 121 | assert.deepEqual(changes[1].fruits._diff, { oranges: 6 }) 122 | assert.deepEqual(changes[2], { 123 | customers: 10, 124 | fruits: { apples: 4, oranges: 6 } 125 | }) 126 | assert.deepEqual(changes[2]._diff, { fruits: { apples: 4 } }) 127 | assert.deepEqual(changes[2].fruits._diff, { apples: 4 }) 128 | 129 | assert.deepEqual(initialState.fruits, { 130 | apples: 3, oranges: 5 131 | }) 132 | assert.deepEqual(fruitChanges[0], { 133 | apples: 3, oranges: 6 134 | }) 135 | assert.deepEqual(fruitChanges[0]._diff, { oranges: 6 }) 136 | assert.deepEqual(fruitChanges[1], { 137 | apples: 4, oranges: 6 138 | }) 139 | assert.deepEqual(fruitChanges[1]._diff, { apples: 4 }) 140 | 141 | assert.equal(changes[1].fruits, changes[0].fruits, 142 | "unchanged properties are the same value") 143 | 144 | assert.end() 145 | }) 146 | 147 | test("observ struct with blackList", function t(assert) { 148 | assert.throws(function () { 149 | ObservHash({ 150 | name: Observ("foo") 151 | }); 152 | }, /cannot create/); 153 | 154 | assert.throws(function () { 155 | ObservHash({ 156 | length: Observ("foo") 157 | }); 158 | }, /cannot create/); 159 | 160 | assert.end() 161 | }) 162 | 163 | test("supports two way data binding", function t(assert) { 164 | var obs = ObservHash({ 165 | foo: Observ("bar") 166 | }); 167 | 168 | obs.foo.set("bar2") 169 | 170 | assert.equal(obs().foo, "bar2") 171 | assert.equal(obs.foo(), "bar2") 172 | 173 | obs.set({ foo: "bar3" }) 174 | 175 | assert.equal(obs().foo, "bar3") 176 | assert.equal(obs.foo(), "bar3") 177 | 178 | assert.end() 179 | }) 180 | 181 | test("two way data binding doesnt emit twice", function t(assert) { 182 | var obs = ObservHash({ 183 | foo: Observ("bar") 184 | }) 185 | 186 | var values = [] 187 | obs.foo(function (v) { 188 | values.push(v) 189 | }) 190 | 191 | obs.set({ foo: "bar2" }) 192 | obs.set({ foo: "bar2" }) 193 | 194 | assert.equal(values.length, 1) 195 | assert.equal(values[0], "bar2") 196 | 197 | assert.end() 198 | }) 199 | 200 | test("support plain values", function t(assert) { 201 | var obs = ObservHash({ 202 | foo: Observ("bar"), 203 | baz: "plain value" 204 | }) 205 | 206 | obs.set({ foo: "bar2", baz: "plain value" }) 207 | 208 | assert.equal(obs().foo, "bar2") 209 | assert.equal(obs().baz, "plain value") 210 | assert.equal(obs.foo(), "bar2") 211 | 212 | assert.end() 213 | }) 214 | 215 | 216 | test("_diff is correct with 2way bind", function t(assert) { 217 | var obs = ObservHash({ 218 | foo: Observ("bar") 219 | }) 220 | 221 | var values = [] 222 | obs(function (v) { 223 | values.push(v) 224 | }) 225 | 226 | obs.set({ foo: "bar2" }) 227 | 228 | assert.equal(obs().foo, "bar2") 229 | assert.equal(obs.foo(), "bar2") 230 | 231 | assert.equal(values.length, 1) 232 | assert.deepEqual(values[0], { 233 | foo: "bar2" 234 | }) 235 | assert.deepEqual(values[0]._diff, { foo: "bar2" }) 236 | 237 | assert.end() 238 | }) 239 | --------------------------------------------------------------------------------