├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── jsdoc.json ├── logo.png ├── package-lock.json ├── package.json ├── src └── index.js └── test ├── fixtures ├── loader.json └── user.json └── index.js /.editorconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fm-ph/quark-state/b88dfc46ae4cb1c62d73c26f8c93c3a27b3dda83/.editorconfig -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | docs 4 | lib 5 | .nyc_output 6 | .history 7 | .vscode 8 | *.log 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | docs 3 | .nyc_output 4 | .vscode 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: false 7 | node_js: 8 | - 'node' 9 | before_script: 10 | - npm prune 11 | script: 12 | - npm test 13 | after_success: 14 | - npm run semantic-release 15 | branches: 16 | except: 17 | - /^v\d+\.\d+\.\d+$/ 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2017 Heng Patrick, Fabien Motte 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [quark-state](https://github.com/fm-ph/quark-state) 2 | 3 | [![build status][travis-image]][travis-url] 4 | [![stability][stability-image]][stability-url] 5 | [![npm version][npm-image]][npm-url] 6 | [![js-standard-style][standard-image]][standard-url] 7 | [![semantic-release][semantic-release-image]][semantic-release-url] 8 | 9 | Simple state manager based on [__Singleton__](https://en.wikipedia.org/wiki/Singleton_pattern) design pattern. 10 | 11 | ___This package is part of `quark` framework but it can be used independently.___ 12 | 13 | ## Installation 14 | 15 | [![NPM](https://nodei.co/npm/quark-state.png)](https://www.npmjs.com/package/quark-state) 16 | 17 | ```sh 18 | npm install quark-state --save 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### Basic 24 | 25 | Initialize a container and set/get a prop. 26 | 27 | ```js 28 | import State from 'quark-state' 29 | 30 | const initialUserState = { 31 | 'name': 'John Doe', 32 | 'age': 36, 33 | 'location': { 34 | 'latitude': 34.564756, 35 | 'longitude': 32.804872 36 | } 37 | } 38 | 39 | // Initialize a container 40 | State.initContainer('USER', initialUserState) 41 | 42 | // Get a prop from a container 43 | const name = State.get('USER.name') // = 'John Doe' 44 | 45 | // Set a prop on a container 46 | State.set('USER.age', 40) 47 | ``` 48 | 49 | ### Container 50 | 51 | #### Init a container 52 | 53 | ```js 54 | import State from 'quark-state' 55 | 56 | const initialContainerState = {} 57 | State.initContainer('CONTAINER', initialContainerState) 58 | ``` 59 | 60 | #### Destroy a container 61 | 62 | ```js 63 | import State from 'quark-state' 64 | 65 | State.destroyContainer('CONTAINER') 66 | ``` 67 | 68 | ### Clear 69 | 70 | Clear the State by destroying all containers 71 | 72 | ```js 73 | import State from 'quark-state' 74 | 75 | const initialContainerState = {} 76 | State.initContainer('CONTAINER', initialContainerState) 77 | 78 | State.clear() 79 | ``` 80 | 81 | ### Set 82 | 83 | #### Set a (deep) prop 84 | 85 | ```js 86 | import State from 'quark-state' 87 | 88 | const initialContainerState = { 89 | 'deep': { 90 | 'prop': true 91 | } 92 | } 93 | State.initContainer('CONTAINER', initialContainerState) 94 | 95 | State.set('CONTAINER.deep.prop', false) 96 | ``` 97 | 98 | #### Set an object prop (merge) 99 | 100 | ```js 101 | import State from 'quark-state' 102 | 103 | const initialContainerState = { 104 | 'deep': { 105 | 'prop': { 106 | 'integer': 10, 107 | 'boolean': true 108 | } 109 | } 110 | } 111 | State.initContainer('CONTAINER', initialContainerState) 112 | 113 | // By default, it will merge the two objects 114 | State.set('CONTAINER.deep.prop', { 115 | 'integer': 20, 116 | 'string': 'foo' 117 | }) 118 | ``` 119 | 120 | #### Set an object prop (overwrite) 121 | 122 | ```js 123 | import State from 'quark-state' 124 | 125 | const initialContainerState = { 126 | 'deep': { 127 | 'prop': { 128 | 'integer': 10, 129 | 'boolean': true 130 | } 131 | } 132 | } 133 | State.initContainer('CONTAINER', initialContainerState) 134 | 135 | // If you set the third argument to true, it will overwrite the prop value 136 | State.set('CONTAINER.deep.prop', { 'integer': 20 }, true) 137 | ``` 138 | 139 | ### Get 140 | 141 | ```js 142 | import State from 'quark-state' 143 | 144 | const initialContainerState = { 145 | 'boolean': true 146 | } 147 | State.initContainer('CONTAINER', initialContainerState) 148 | 149 | State.get('CONTAINER.boolean') // = true 150 | ``` 151 | 152 | ### Has 153 | 154 | Check if the given query exists (container or prop) 155 | 156 | ```js 157 | import State from 'quark-state' 158 | 159 | const initialContainerState = { 160 | 'string': 'foo' 161 | } 162 | State.initContainer('CONTAINER', initialContainerState) 163 | 164 | State.has('CONTAINER') // = true 165 | State.has('CONTAINER.foo') // = true 166 | State.has('CONTAINER.doesNotExist') // = false 167 | ``` 168 | 169 | ### On change 170 | 171 | #### Add 172 | 173 | When a prop is modified, call a callback with old and new values 174 | 175 | ```js 176 | import State from 'quark-state' 177 | 178 | const initialContainerState = { 179 | 'string': 'foo', 180 | 'deep': { 181 | 'prop': true 182 | } 183 | } 184 | State.initContainer('CONTAINER', initialContainerState) 185 | 186 | // When a prop is modified, callback is called 187 | State.onChange('CONTAINER.string', (oldVal, newVal) => { }) // oldVal = 'foo', newVal = 'bar' 188 | State.set('CONTAINER.string', 'bar') 189 | 190 | // When a deep prop is modified, it also triggers parent callback 191 | State.onChange('CONTAINER', (oldVal, newVal) => { }) // oldVal = true, newVal = false 192 | State.set('CONTAINER.deep.prop', false) 193 | ``` 194 | 195 | #### Remove 196 | 197 | ```js 198 | import State from 'quark-state' 199 | 200 | const initialContainerState = { 201 | 'string': 'foo', 202 | 'deep': { 203 | 'prop': true 204 | } 205 | } 206 | State.initContainer('CONTAINER', initialContainerState) 207 | 208 | const callback = (oldVal, newVal) => { } // Won't be trigger 209 | 210 | State.onChange('CONTAINER.string', callback) 211 | State.removeChangeCallback('CONTAINER.string', callback) 212 | 213 | State.set('CONTAINER.string', 'bar') 214 | ``` 215 | 216 | ## API 217 | 218 | See [https://fm-ph.github.io/quark-state/](https://fm-ph.github.io/quark-state/) 219 | 220 | ## Build 221 | 222 | To build the sources with `babel` in `./lib` directory : 223 | 224 | ```sh 225 | npm run build 226 | ``` 227 | 228 | ## Documentation 229 | 230 | To generate the `JSDoc` : 231 | 232 | ```sh 233 | npm run docs 234 | ``` 235 | 236 | To generate the documentation and deploy on `gh-pages` branch : 237 | 238 | ```sh 239 | npm run docs:deploy 240 | ``` 241 | 242 | ## Testing 243 | 244 | To run the tests, first clone the repository and install its dependencies : 245 | 246 | ```sh 247 | git clone https://github.com/fm_ph/quark-state.git 248 | cd quark-state 249 | npm install 250 | ``` 251 | 252 | Then, run the tests : 253 | 254 | ```sh 255 | npm test 256 | ``` 257 | 258 | To watch (test-driven development) : 259 | 260 | ```sh 261 | npm run test:watch 262 | ``` 263 | 264 | For coverage : 265 | 266 | ```sh 267 | npm run test:coverage 268 | ``` 269 | 270 | ## License 271 | 272 | MIT [License](LICENSE.md) © [Patrick Heng](http://hengpatrick.fr/) [Fabien Motte](http://fabienmotte.com/) 273 | 274 | [travis-image]: https://img.shields.io/travis/fm-ph/quark-state/master.svg?style=flat-square 275 | [travis-url]: http://travis-ci.org/fm-ph/quark-state 276 | [stability-image]: https://img.shields.io/badge/stability-stable-brightgreen.svg?style=flat-square 277 | [stability-url]: https://nodejs.org/api/documentation.html#documentation_stability_index 278 | [npm-image]: https://img.shields.io/npm/v/quark-state.svg?style=flat-square 279 | [npm-url]: https://npmjs.org/package/quark-state 280 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 281 | [standard-url]: https://github.com/feross/standard 282 | [semantic-release-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square 283 | [semantic-release-url]: https://github.com/semantic-release/semantic-release 284 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": [ 5 | "jsdoc" 6 | ] 7 | }, 8 | "source": { 9 | "include": [ 10 | "README.md", 11 | "src" 12 | ], 13 | "includePattern": ".js$", 14 | "excludePattern": "(node_modules/|docs)" 15 | }, 16 | "plugins": [ 17 | "plugins/markdown" 18 | ], 19 | "templates": { 20 | "cleverLinks": false, 21 | "monospaceLinks": true, 22 | "useLongnameInNav": false 23 | }, 24 | "opts": { 25 | "destination": "./docs", 26 | "encoding": "utf8", 27 | "template": "node_modules/minami" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fm-ph/quark-state/b88dfc46ae4cb1c62d73c26f8c93c3a27b3dda83/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quark-state", 3 | "version": "0.0.0-development", 4 | "description": "Simple state manager based on Singleton design pattern", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "BABEL_ENV=production babel src -d lib", 8 | "docs": "node_modules/.bin/jsdoc --configure jsdoc.json --verbose", 9 | "docs:deploy": "npm run docs && gh-pages -d docs/ -m 'docs: update'", 10 | "test": "ava", 11 | "test:watch": "ava --watch --verbose", 12 | "test:coverage": "nyc npm test", 13 | "prepublishOnly": "npm test && npm run build", 14 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 15 | }, 16 | "keywords": [ 17 | "quark", 18 | "state", 19 | "singleton" 20 | ], 21 | "author": "fm_ph", 22 | "contributors": [ 23 | "Patrick Heng (http://hengpatrick.fr/)", 24 | "Fabien Motte (http://fabienmotte.com/)" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/fm-ph/quark-state.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/fm-ph/quark-state/issues" 32 | }, 33 | "homepage": "https://github.com/fm-ph/quark-state", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "ava": "^0.18.1", 37 | "babel-cli": "^6.22.2", 38 | "babel-plugin-add-module-exports": "^0.2.1", 39 | "babel-plugin-transform-object-rest-spread": "^6.22.0", 40 | "babel-plugin-transform-runtime": "^6.22.0", 41 | "babel-preset-es2015": "^6.22.0", 42 | "babelify": "^7.3.0", 43 | "gh-pages": "^0.12.0", 44 | "jsdoc": "^3.4.3", 45 | "minami": "nijikokun/minami", 46 | "nyc": "^10.1.2", 47 | "semantic-release": "^6.3.2" 48 | }, 49 | "babel": { 50 | "presets": [ 51 | "es2015" 52 | ], 53 | "plugins": [ 54 | "add-module-exports", 55 | "transform-object-rest-spread" 56 | ], 57 | "ignore": "test.js", 58 | "env": { 59 | "development": { 60 | "sourceMaps": "inline", 61 | "plugins": [ 62 | "transform-runtime" 63 | ] 64 | } 65 | } 66 | }, 67 | "ava": { 68 | "files": [ 69 | "test/*.js" 70 | ], 71 | "require": [ 72 | "babel-core/register" 73 | ] 74 | }, 75 | "dependencies": { 76 | "lodash.clonedeep": "^4.5.0", 77 | "lodash.isequal": "4.5.0", 78 | "quark-signal": "1.1.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Signal from 'quark-signal' 2 | 3 | import isEqual from 'lodash.isequal' 4 | import cloneDeep from 'lodash.clonedeep' 5 | 6 | /** 7 | * State class 8 | * 9 | * @class 10 | * 11 | * @license {@link https://opensource.org/licenses/MIT|MIT} 12 | * 13 | * @author Patrick Heng 14 | * @author Fabien Motte 15 | * 16 | * @example 17 | * const initialContainerState = { 18 | * 'foo': 'bar' 19 | * } 20 | * 21 | * State.initContainer('CONTAINER', initialContainerState) 22 | * const foo = State.get('CONTAINER.foo') // = 'bar' 23 | */ 24 | class State { 25 | /** 26 | * Creates an instance of State 27 | * 28 | * @constructor 29 | */ 30 | constructor () { 31 | /** 32 | * @type object 33 | * @private 34 | */ 35 | this._containers = [] 36 | } 37 | 38 | /** 39 | * Get a value 40 | * 41 | * @param {string} query Query string 42 | * 43 | * @returns {any} Value 44 | * 45 | * @throws {Error} Cannot get a value from a container that does not exist 46 | */ 47 | get (query) { 48 | const { container, splittedQuery } = this._parseStateQuery(query) 49 | 50 | if (typeof container === 'undefined') { 51 | throw new Error('State.get() : Cannot get a value from a container that does not exist') 52 | } 53 | 54 | let value = container.tree 55 | 56 | if (splittedQuery.length > 1) { 57 | for (let i = 1, l = splittedQuery.length; i < l; i++) { 58 | value = value[splittedQuery[i]] 59 | 60 | if (typeof value === 'undefined' || value === null) { 61 | break 62 | } 63 | } 64 | } 65 | 66 | return value 67 | } 68 | 69 | /** 70 | * Set a value 71 | * 72 | * @param {string} query Query string 73 | * @param {any} value Value to set 74 | * @param {boolean} [overwrite=false] Flag to overwrite an object 75 | * 76 | * @throws {Error} Cannot set a value on a container that does not exist 77 | */ 78 | set (query, value, overwrite = false) { 79 | const { container, containerId, splittedQuery } = this._parseStateQuery(query) 80 | 81 | if (typeof container === 'undefined') { 82 | throw new Error('State.set() : Cannot set a value on a container that does not exist') 83 | } 84 | 85 | let target = container.tree 86 | const slicedQuery = splittedQuery.slice(1) 87 | 88 | for (let i = 0, l = slicedQuery.length; i < l; i++) { 89 | const prop = slicedQuery[i] 90 | const oldVal = target[prop] 91 | 92 | if (typeof target[prop] !== 'object' && target[prop] !== null) { 93 | target[prop] = {} 94 | } 95 | 96 | if (i === slicedQuery.length - 1) { 97 | if (typeof oldVal === 'undefined' || typeof value !== 'object' || value === null || overwrite) { 98 | target[prop] = value 99 | } else { 100 | target[prop] = { 101 | ...oldVal, 102 | ...value 103 | } 104 | } 105 | } 106 | 107 | target = target[prop] 108 | 109 | let signalId = containerId 110 | for (let j = 0; j <= slicedQuery.length; j++) { 111 | if (typeof container.signals[signalId] !== 'undefined') { 112 | if (!isEqual(oldVal, target)) { 113 | container.signals[signalId].dispatch(oldVal, target) 114 | } 115 | } 116 | 117 | signalId += `_${slicedQuery[j]}` 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * Has a value 124 | * 125 | * @param {string} query Query string 126 | * 127 | * @returns {boolean} True if a value is found, false otherwise 128 | */ 129 | has (query) { 130 | const { container } = this._parseStateQuery(query) 131 | 132 | if (typeof container === 'undefined') { 133 | return false 134 | } 135 | 136 | const value = this.get(query) 137 | 138 | return (typeof value !== 'undefined' && value !== null) 139 | } 140 | 141 | /** 142 | * Clear all containers 143 | */ 144 | clear () { 145 | for (let containerId in this._containers) { 146 | this.destroyContainer(containerId) 147 | } 148 | 149 | this._containers = {} 150 | } 151 | 152 | /** 153 | * Add a callback on value change 154 | * 155 | * @param {string} query Query string 156 | * @param {function} callback Callback 157 | * 158 | * @throws {TypeError} Second argument must be a Function 159 | * @throws {Error} Cannot add a change callback on a container that does not exist 160 | */ 161 | onChange (query, callback) { 162 | if (typeof callback !== 'function') { 163 | throw new TypeError('State.onChange() : Second argument must be a Function') 164 | } 165 | 166 | const { container, containerId, splittedQuery } = this._parseStateQuery(query) 167 | 168 | if (typeof container === 'undefined') { 169 | throw new Error('State.onChange() : Cannot add a change callback on a container that does not exist') 170 | } 171 | 172 | let signalId = containerId 173 | 174 | for (let i = 1, l = splittedQuery.length; i < l; i++) { 175 | signalId += `_${splittedQuery[i]}` 176 | } 177 | 178 | if (typeof container.signals[signalId] === 'undefined') { 179 | container.signals[signalId] = new Signal() 180 | } 181 | 182 | container.signals[signalId].add(callback) 183 | } 184 | 185 | /** 186 | * Remove callback on change 187 | * 188 | * @param {string} query Query string 189 | * @param {function} callback Callback 190 | * 191 | * @throws {TypeError} Second argument must be a Function 192 | * @throws {Error} Cannot remove a change callback on a container that does not exist 193 | * @throws {Error} No signal found to remove a change callback with query : 'CONTAINER.query' 194 | */ 195 | removeChangeCallback (query, callback) { 196 | if (typeof callback !== 'function') { 197 | throw new TypeError('State.removeChangeCallback() : Second argument must be a Function') 198 | } 199 | 200 | const { container } = this._parseStateQuery(query) 201 | 202 | if (typeof container === 'undefined') { 203 | throw new Error('State.removeChangeCallback() : Cannot remove a change callback on a container that does not exist') 204 | } 205 | 206 | const signalId = query.replace(/\./g, '_') 207 | 208 | if (typeof container.signals[signalId] === 'undefined' || !(container.signals[signalId] instanceof Signal)) { 209 | throw new Error(`State.removeChangeCallback() : No signal found to remove a change callback with query : '${query}'`) 210 | } 211 | 212 | container.signals[signalId].remove(callback) 213 | } 214 | 215 | /** 216 | * Initialize a container 217 | * 218 | * @param {string} containerId Container id 219 | * @param {object} value Object to initialize the container 220 | * 221 | * @throws {TypeError} Second argument must be an Object 222 | */ 223 | initContainer (containerId, value) { 224 | if (value === null || typeof value !== 'object') { 225 | throw new TypeError('State.initContainer() : Second argument must be an Object') 226 | } 227 | 228 | this._containers[containerId] = {} 229 | 230 | this._containers[containerId].tree = cloneDeep(value) 231 | this._containers[containerId].signals = {} 232 | } 233 | 234 | /** 235 | * Destroy a container 236 | * 237 | * @param {string} containerId Container id 238 | * 239 | * @throws {Error} Cannot destroy a container that does not exist 240 | */ 241 | destroyContainer (containerId) { 242 | if (typeof this._containers[containerId] === 'undefined') { 243 | throw new Error('State.destroyContainer() : Cannot destroy a container that does not exist') 244 | } 245 | 246 | for (let signalProp in this._containers[containerId].signals) { 247 | this._containers[containerId].signals[signalProp].removeAll() 248 | this._containers[containerId].signals[signalProp] = null 249 | } 250 | 251 | this._containers[containerId] = null 252 | delete this._containers[containerId] 253 | } 254 | 255 | /** 256 | * Parse state query 257 | * 258 | * @private 259 | * 260 | * @param {string} query Query string 261 | * 262 | * @property {object} container Container 263 | * @property {string} containerId Container id 264 | * @property {prop} container Container 265 | * @property {array} splittedQuery Splitted query 266 | * 267 | * @throws {TypeError} Query argument must be a string 268 | */ 269 | _parseStateQuery (query) { 270 | if (typeof query !== 'string') { 271 | throw new TypeError('State : Query argument must be a string') 272 | } 273 | 274 | const splittedQuery = query.split('.') 275 | 276 | return { 277 | container: this._containers[splittedQuery[0]], 278 | containerId: splittedQuery[0], 279 | splittedQuery 280 | } 281 | } 282 | } 283 | 284 | export default new State() 285 | -------------------------------------------------------------------------------- /test/fixtures/loader.json: -------------------------------------------------------------------------------- 1 | { 2 | "loaded": false, 3 | "progress": 0 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "58a0f79c57e96101e6814cb1", 3 | "index": 0, 4 | "guid": "4b06eda6-874e-49f7-9c62-8b87c79767a7", 5 | "isActive": true, 6 | "balance": "$1,707.15", 7 | "picture": "http://placehold.it/32x32", 8 | "age": 36, 9 | "eyeColor": "brown", 10 | "name": "Maryann Bowen", 11 | "gender": "female", 12 | "company": "PETIGEMS", 13 | "email": "maryannbowen@petigems.com", 14 | "phone": "+1 (802) 546-3959", 15 | "address": "146 Goodwin Place, Warren, Nevada, 111", 16 | "about": "Commodo in aute ullamco adipisicing proident incididunt reprehenderit quis. Mollit proident tempor occaecat cupidatat elit Lorem enim Lorem nostrud laborum. Adipisicing ut non Lorem excepteur Lorem aliquip sit magna. Reprehenderit nulla qui reprehenderit voluptate ullamco commodo laboris ut incididunt tempor tempor cupidatat nulla cupidatat. Incididunt magna sit veniam eiusmod elit occaecat occaecat mollit dolore ad sunt laboris. Aliqua fugiat magna consequat id officia minim consectetur. Tempor nulla pariatur fugiat eu irure consequat do sunt commodo Lorem veniam.\r\n", 17 | "registered": "2014-02-21T07:16:40 -01:00", 18 | "location": { 19 | "latitude": 34.564756, 20 | "longitude": 32.804872 21 | }, 22 | "tags": [ 23 | "adipisicing", 24 | "cupidatat", 25 | "dolor", 26 | "fugiat", 27 | "et", 28 | "sit", 29 | "in" 30 | ], 31 | "friends": [ 32 | { 33 | "id": 0, 34 | "name": "West Kidd" 35 | }, 36 | { 37 | "id": 1, 38 | "name": "Duffy Strong" 39 | }, 40 | { 41 | "id": 2, 42 | "name": "Joyce Benton" 43 | } 44 | ], 45 | "greeting": "Hello, Maryann Bowen! You have 2 unread messages.", 46 | "favoriteFruit": "banana" 47 | } 48 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import State from '../src/index' 4 | import fixtureUser from './fixtures/user' 5 | import fixtureLoader from './fixtures/loader' 6 | 7 | test.beforeEach(t => { 8 | State.clear() // Clear the State before each test 9 | 10 | // Initialize the State with fixtures data 11 | State.initContainer('USER', fixtureUser) 12 | State.initContainer('LOADER', fixtureLoader) 13 | }) 14 | 15 | /** 16 | * initContainer method 17 | */ 18 | test('initialize a container', t => { 19 | t.deepEqual(State.get('USER'), fixtureUser) 20 | }) 21 | 22 | test('initialize a container with a bad type value throws a type error', t => { 23 | const error = t.throws(() => State.initContainer('TEST', null), TypeError) 24 | 25 | t.is(error.message, 'State.initContainer() : Second argument must be an Object') 26 | }) 27 | 28 | /** 29 | * destroyContainer method 30 | */ 31 | test('destroy a container', t => { 32 | State.destroyContainer('USER') 33 | t.false(State.has('USER')) 34 | }) 35 | 36 | test('destroy a container that does not exist throws an error', t => { 37 | const error = t.throws(() => State.destroyContainer('DOES_NOT_EXIST'), Error) 38 | 39 | t.is(error.message, 'State.destroyContainer() : Cannot destroy a container that does not exist') 40 | }) 41 | 42 | /** 43 | * clear method 44 | */ 45 | test('clear the state', t => { 46 | State.clear() 47 | t.deepEqual(State._containers, {}) 48 | }) 49 | 50 | /** 51 | * set/get methods 52 | */ 53 | test('set/get a prop', t => { 54 | State.set('USER.gender', 'male') 55 | t.is(State.get('USER.gender'), 'male') 56 | }) 57 | 58 | test('set/get a deep prop', t => { 59 | State.set('USER.location.latitude', 10) 60 | t.is(State.get('USER.location.latitude'), 10) 61 | }) 62 | 63 | test('set an object prop that already exists (merge)', t => { 64 | State.set('USER.location', { 65 | 'latitude': 10 66 | }) 67 | 68 | t.deepEqual(State.get('USER.location'), { 69 | 'latitude': 10, 70 | 'longitude': fixtureUser.location.longitude 71 | }) 72 | }) 73 | 74 | test('set an object prop that already exists (overwrite)', t => { 75 | State.set('USER.location', { 76 | 'latitude': 10 77 | }, true) 78 | 79 | t.deepEqual(State.get('USER.location'), { 80 | 'latitude': 10 81 | }) 82 | }) 83 | 84 | /** 85 | * get method 86 | */ 87 | test('get a prop from a container that does not exist throws an error', t => { 88 | const error = t.throws(() => State.get('DOES_NOT_EXIST.prop'), Error) 89 | 90 | t.is(error.message, 'State.get() : Cannot get a value from a container that does not exist') 91 | }) 92 | 93 | /** 94 | * set method 95 | */ 96 | test('set a prop on a container that does not exist throws an error', t => { 97 | const error = t.throws(() => State.set('DOES_NOT_EXIST.prop', true), Error) 98 | 99 | t.is(error.message, 'State.set() : Cannot set a value on a container that does not exist') 100 | }) 101 | 102 | /** 103 | * has method 104 | */ 105 | test('has a container that exists', t => { 106 | t.true(State.has('LOADER')) 107 | }) 108 | 109 | test('has a container that does not exist', t => { 110 | t.false(State.has('DOES_NOT_EXIST')) 111 | }) 112 | 113 | test('has a prop that exists', t => { 114 | t.true(State.has('LOADER.loaded')) 115 | }) 116 | 117 | test('has a prop that does not exist', t => { 118 | t.false(State.has('LOADER.doesNotExist')) 119 | }) 120 | 121 | /** 122 | * onChange method 123 | */ 124 | test.cb('when a prop change the callback is called', t => { 125 | State.onChange('LOADER.loaded', (oldVal, newVal) => { 126 | t.false(oldVal) 127 | t.true(newVal) 128 | t.end() 129 | }) 130 | 131 | State.set('LOADER.loaded', true) 132 | }) 133 | 134 | test('when a prop does not change nothing is called', t => { 135 | State.onChange('LOADER.loaded', (oldVal, newVal) => { 136 | t.fail() 137 | }) 138 | 139 | State.set('LOADER.loaded', false) 140 | }) 141 | 142 | test.cb('when a deep prop change the callback is called', t => { 143 | State.onChange('USER.location.latitude', (oldVal, newVal) => { 144 | t.is(oldVal, fixtureUser.location.latitude) 145 | t.is(newVal, 10) 146 | t.end() 147 | }) 148 | 149 | State.set('USER.location.latitude', 10) 150 | }) 151 | 152 | test.cb('when a deep prop change a parent object callback is called', t => { 153 | State.onChange('USER.location', (oldVal, newVal) => { 154 | t.is(oldVal, fixtureUser.location.latitude) 155 | t.is(newVal, 10) 156 | t.end() 157 | }) 158 | 159 | State.set('USER.location.latitude', 10) 160 | }) 161 | 162 | test.cb('when a deep prop change a container callback is called', t => { 163 | State.onChange('USER', (oldVal, newVal) => { 164 | t.is(oldVal, fixtureUser.location.longitude) 165 | t.is(newVal, 10) 166 | t.end() 167 | }) 168 | 169 | State.set('USER.location.longitude', 10) 170 | }) 171 | 172 | test.cb('when a deep object prop change a callback is called', t => { 173 | State.onChange('USER', (oldVal, newVal) => { 174 | t.deepEqual(oldVal, fixtureUser.location) 175 | t.deepEqual(newVal, { 'latitude': 10, 'longitude': fixtureUser.location.longitude }) 176 | t.end() 177 | }) 178 | 179 | State.set('USER.location', { 180 | 'latitude': 10, 181 | 'longitude': fixtureUser.location.longitude 182 | }) 183 | }) 184 | 185 | test('add a change callback with a bad type value throws an error', t => { 186 | const error = t.throws(() => State.onChange('USER', null), Error) 187 | 188 | t.is(error.message, 'State.onChange() : Second argument must be a Function') 189 | }) 190 | 191 | test('add a change callback on a container that does not exist throws an error', t => { 192 | const error = t.throws(() => State.onChange('DOES_NOT_EXIST', (oldVal, newVal) => { }), Error) 193 | 194 | t.is(error.message, 'State.onChange() : Cannot add a change callback on a container that does not exist') 195 | }) 196 | 197 | /** 198 | * removeChangeCallback method 199 | */ 200 | test('remove a change callback on a prop', t => { 201 | const cb = (oldVal, newVal) => { t.fail() } 202 | State.onChange('LOADER.loaded', cb) 203 | 204 | t.is(State._containers['LOADER'].signals['LOADER_loaded']._listeners.length, 1) 205 | State.removeChangeCallback('LOADER.loaded', cb) 206 | t.is(State._containers['LOADER'].signals['LOADER_loaded']._listeners.length, 0) 207 | }) 208 | 209 | test('remove a change callback on a deep prop', t => { 210 | const cb = (oldVal, newVal) => { t.fail() } 211 | State.onChange('USER.location.latitude', cb) 212 | 213 | t.is(State._containers['USER'].signals['USER_location_latitude']._listeners.length, 1) 214 | State.removeChangeCallback('USER.location.latitude', cb) 215 | t.is(State._containers['USER'].signals['USER_location_latitude']._listeners.length, 0) 216 | }) 217 | 218 | test('remove a change callback on a container', t => { 219 | const cb = (oldVal, newVal) => { t.fail() } 220 | State.onChange('USER', cb) 221 | 222 | t.is(State._containers['USER'].signals['USER']._listeners.length, 1) 223 | State.removeChangeCallback('USER', cb) 224 | t.is(State._containers['USER'].signals['USER']._listeners.length, 0) 225 | }) 226 | 227 | test('remove a change callback with a bad type throws a type error', t => { 228 | const error = t.throws(() => State.removeChangeCallback('USER', null), TypeError) 229 | 230 | t.is(error.message, 'State.removeChangeCallback() : Second argument must be a Function') 231 | }) 232 | 233 | test('remove a change callback on a container that does not exist throws an error', t => { 234 | const error = t.throws(() => State.removeChangeCallback('DOES_NOT_EXIST', (oldVal, newVal) => { }), Error) 235 | 236 | t.is(error.message, 'State.removeChangeCallback() : Cannot remove a change callback on a container that does not exist') 237 | }) 238 | 239 | test('remove a change callback with a query that does not have a signal throws an error', t => { 240 | const error = t.throws(() => State.removeChangeCallback('USER.location', (oldVal, newVal) => { }), Error) 241 | 242 | t.is(error.message, `State.removeChangeCallback() : No signal found to remove a change callback with query : 'USER.location'`) 243 | }) 244 | 245 | /** 246 | * _parseStateQuery method 247 | */ 248 | test('parse a query with a bad type throws an error', t => { 249 | const error = t.throws(() => State._parseStateQuery(null), TypeError) 250 | 251 | t.is(error.message, 'State : Query argument must be a string') 252 | }) 253 | --------------------------------------------------------------------------------