├── .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 | # [
](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 | [](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 |
--------------------------------------------------------------------------------