├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .gitlab-ci.yml ├── .npmignore ├── LICENSE ├── README.md ├── bili.config.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── plugins │ ├── devtools.ts │ └── logger.ts ├── store.spec.ts ├── store.ts ├── typings.ts ├── wrapper.spec.ts └── wrapper.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | indent_size = 2 10 | indent_style = space 11 | 12 | # Tab indentation (no size specified) 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | 'env': { 4 | 'browser': true, 5 | 'es6': true, 6 | 'jest': true 7 | }, 8 | 'extends': 'standard', 9 | 'globals': { 10 | 'Atomics': 'readonly', 11 | 'SharedArrayBuffer': 'readonly' 12 | }, 13 | 'parserOptions': { 14 | 'ecmaVersion': 2018, 15 | 'sourceType': 'module' 16 | }, 17 | 'rules': { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .rpt2_cache 4 | coverage 5 | types 6 | examples 7 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:lts 2 | 3 | stages: 4 | - lint 5 | - tests 6 | - build 7 | 8 | before_script: 9 | - npm i 10 | - npm i vue@2.6.10 --no-save 11 | 12 | # lint stage 13 | lint: 14 | stage: lint 15 | script: 16 | - npm run lint 17 | 18 | # test stage 19 | test: 20 | stage: tests 21 | script: 22 | - npm run test:coverage 23 | 24 | # build stage 25 | build: 26 | stage: build 27 | script: 28 | - npm run build 29 | 30 | # publish 31 | publish: 32 | stage: build 33 | script: 34 | - npm run publish 35 | when: manual 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | **/tsconfig.json 3 | **/jest.config.js 4 | src 5 | coverage 6 | examples 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mathieu DARTIGUES 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## vue-reactive-store 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/mdartic/vue-reactive-store.svg)](https://greenkeeper.io/) 4 | ![](https://gitlab.com/mad-z/vue-reactive-store-ci-cd/badges/master/pipeline.svg) 5 | ![](https://gitlab.com/mad-z/vue-reactive-store-ci-cd/badges/master/coverage.svg) 6 | 7 | *Vue.js* (only) library for **managing a centralized state**, inspired by Vue.js and VueX. 8 | 9 | Without `mutations`, and with async `actions` mutating directly the state. 10 | 11 | We could talk about `mutactions`. 12 | 13 | Compatible with [`vue-devtools`](https://github.com/vuejs/vue-devtools/) by using a plugin. 14 | 15 | This library is in a WIP state. 16 | 17 | You can create issues to ask for some features, or if you find bugs. 18 | 19 | If feedbacks are goods, I'll write a better documentation :-) 20 | with english examples and take care of this library. 21 | 22 | ### Core concepts 23 | 24 | `vue-reactive-store` is a library trying to make easier 25 | the centralization of your app's data. 26 | 27 | A store is composed of : 28 | * a **name** 29 | * a **state**, that will evolve in time (think the `data` of a Vue.js instance) 30 | * **computed** properties based on this state (think the `computed` of a Vue.js instance, or the `getters` for VueX) 31 | * **actions** that will make API calls, mutate the state, ... (think `actions` for VueX, but with `mutations` inside) 32 | * **watch(ers)** that could react to state / computed evolutions (same as `watch` for Vue.js instance) 33 | * **plugins**, trigerred for state evolution, computed properties, actions / watchers trigerred 34 | * **modules**, aka sub-stores, namespaced 35 | * ***props***, like Vue.js instances, but, just an idea for the moment 36 | 37 | ### Why creating an alternative ? 38 | 39 | I think we can do a store simpler than VueX. 40 | 41 | With VS Code and Intellisense, I would like my IDE 42 | tell me what's in my store, instead of calling dispatch functions 43 | or mapGetters / mapState functions. 44 | 45 | We could trigger actions by importing them directly where we want to use them. 46 | Not by dispatching an action with a string composed by his namespace / function name. 47 | 48 | And I think we can do better with TypeScript, to help us with typings. 49 | 50 | For the moment, autocompletion is not as good as I want. I'm working on it. 51 | 52 | ### How to use it 53 | 54 | I hope the use of TypeScript will benefit for better understanding. 55 | 56 | First, install `vue-reactive-store` in your Vue.js app. 57 | 58 | ``` 59 | npm i vue-reactive-store 60 | ``` 61 | 62 | Add a store as a JS object, and transform it by creating 63 | a `VueReactiveStore` instance. 64 | 65 | ```js 66 | // src/store.js 67 | import VueReactiveStore from 'vue-reactive-store' 68 | 69 | const store = { 70 | state: { 71 | loading: false, 72 | error: null, 73 | data: null 74 | }, 75 | computed: { 76 | myCurrentState() { 77 | if (store.state.loading === true) return 'is loading...' 78 | if (store.state.error) return 'error...' 79 | return 'store seems ok' 80 | } 81 | }, 82 | actions: { 83 | async fetchData() { 84 | store.state.loading = true 85 | try { 86 | // make api call 87 | const response = await myApi.fetchSomeData() 88 | store.state.data = response 89 | } catch (e) { 90 | store.state.error = e 91 | } 92 | store.state.loading = false 93 | } 94 | }, 95 | plugins: [{ 96 | actions: { 97 | after(storeName, actionName, storeState) { 98 | console.log('action is finished, this is my store : ', storeState) 99 | } 100 | } 101 | }] 102 | } 103 | 104 | const reactiveStore = new VueReactiveStore(store) 105 | 106 | export default reactiveStore 107 | ``` 108 | 109 | Finally, use it in your components by importing the store, 110 | and put the data that interest you in the `data` and `computed` 111 | part of your app. 112 | 113 | ```vue 114 | // src/components/myComponent.js 115 | 126 | 127 | 140 | ``` 141 | 142 | That sould do the trick, now your store is reactive, 143 | and you could use it in all the component you want by importing 144 | this object. 145 | But, don't import it everywhere, just use it in your 'top-level' 146 | components to facilitate your project maintenability... 147 | 148 | **IMPORTANT !** 149 | To use the data, you'll have to wire the `state` property of the `store`. 150 | If you wire `store.state.data`, you'll get `null` and your `data` property isn't reactive yet. 151 | 152 | ``` 153 | import store from '../store' 154 | 155 | export default { 156 | data: { 157 | data: store.state.data, // here, you'll always get 'null' 158 | state: store.state // here, state is reactive, and so all children, like data 159 | }, 160 | … 161 | } 162 | ``` 163 | 164 | For computed properties or actions, it'll be fine. 165 | You can wire directly the computed property or the action. 166 | 167 | ### Logger plugin 168 | 169 | There is a logger plugin that logs 170 | * each action trigerred (before / after) 171 | * each mutation on the state (after) 172 | * each computed property recomputed (after) 173 | 174 | To use it, you can do like this : 175 | 176 | ```js 177 | // src/store.js 178 | import VueReactiveStore from 'vue-reactive-store' 179 | import VRSPluginLogger from 'vue-reactive-store/dist/index.esm' 180 | 181 | const store = { 182 | state: { 183 | loading: false, 184 | error: null, 185 | data: null 186 | }, 187 | computed: { 188 | myCurrentState() { 189 | if (store.state.loading === true) return 'is loading...' 190 | if (store.state.error) return 'error...' 191 | return 'store seems ok' 192 | } 193 | }, 194 | actions: { 195 | async fetchData() { 196 | store.state.loading = true 197 | try { 198 | // make api call 199 | const response = await myApi.fetchSomeData() 200 | store.state.data = response 201 | } catch (e) { 202 | store.state.error = e 203 | } 204 | store.state.loading = false 205 | } 206 | } 207 | } 208 | 209 | VueReactiveStore.registerPlugin(VRSPluginLogger); 210 | 211 | const reactiveStore = new VueReactiveStore(store) 212 | 213 | // this call will fetch data 214 | // and the VRSPluginLogger will log 215 | // * the action trigerred (before) 216 | // * the state mutations (after) 217 | // * the computed properties (after) 218 | // * the end of the action (after) 219 | store.actions.fetchData() 220 | 221 | export default store 222 | ``` 223 | 224 | You can also decide to just log action / state / computed without previous/next state. 225 | 226 | ```js 227 | // by default all are true and so are verbose logs 228 | VRSPluginLogger.logSettings.actions = false; 229 | VRSPluginLogger.logSettings.computed = false; 230 | VRSPluginLogger.logSettings.state = false; 231 | ``` 232 | 233 | ### Devtools plugin 234 | 235 | Like VueX, you can debug your store with [vue-devtools](https://github.com/vuejs/vue-devtools/). 236 | 237 | It's not enabled 'by default', and you have to explicitly add the devtools plugin like that : 238 | 239 | ```javascript 240 | import VueReactiveStore from 'vue-reactive-store' 241 | import VRSPluginDevtools from 'vue-reactive-store/dist/devtools.esm' 242 | 243 | const store = { 244 | state: { 245 | loading: false, 246 | error: null, 247 | results: null 248 | }, 249 | actions: { 250 | async fetchData () { 251 | store.state.loading = true 252 | try { 253 | // blablabla 254 | store.state.results = 'pwet' 255 | } catch (e) { 256 | store.state.error = e 257 | } 258 | store.state.loading = false 259 | } 260 | }, 261 | plugins: [ 262 | VRSPluginDevtools 263 | ] 264 | } 265 | 266 | new VueReactiveStore(store) 267 | 268 | export default store 269 | ``` 270 | 271 | Then you could observe there is a store detected in the VueX tab of vue-devtools. 272 | 273 | **Time Travel** is normally ok, but maybe there are some lacks for **Commit mutations**. 274 | 275 | Submit an issue if you find bugs. 276 | 277 | ### Next episodes 278 | 279 | * finishing blog articles (FR) 280 | * release a plugin for storing data in localStorage 281 | * release a plugin for history (undo / redo) 282 | * listen to community needs 283 | -------------------------------------------------------------------------------- /bili.config.ts: -------------------------------------------------------------------------------- 1 | // bili.config.ts 2 | import { Config } from 'bili' 3 | 4 | const config: Config = { 5 | banner: true, 6 | input: [ 7 | 'src/index.ts', 8 | 'src/plugins/logger.ts', 9 | 'src/plugins/devtools.ts' 10 | ], 11 | plugins: { 12 | typescript2: { 13 | clean: true, 14 | // check: false, 15 | useTsconfigDeclarationDir: true 16 | } 17 | } 18 | } 19 | 20 | export default config 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "transform": { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | "testMatch": [ 9 | "**/__tests__/**/*.js?(x)", 10 | "**/?(*.)+(spec|test).js?(x)", 11 | "**/__tests__/**/*.ts?(x)", 12 | "**/?(*.)+(spec|test).ts?(x)" 13 | ], 14 | "moduleFileExtensions": [ 15 | "ts", 16 | "tsx", 17 | "js", 18 | "jsx", 19 | "json", 20 | "node" 21 | ], 22 | "testEnvironment": "node" 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-reactive-store", 3 | "version": "0.0.20", 4 | "description": "Structure your Vue.js data in a centralized way, with a reactive data store. Inspired by VueX and Vue.js .", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.esm.js", 7 | "types": "./types/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint src/**/*.ts", 10 | "build": "bili --format cjs --format es", 11 | "test": "jest --watch", 12 | "test:coverage": "jest --coverage" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+ssh://git@github.com:mdartic/vue-reactive-store.git" 17 | }, 18 | "keywords": [ 19 | "state", 20 | "management", 21 | "vue" 22 | ], 23 | "author": "Mathieu DARTIGUES", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/mdartic/vue-reactive-store/issues" 27 | }, 28 | "homepage": "https://github.com/mdartic/vue-reactive-store#readme", 29 | "peerDependencies": { 30 | "vue": "^2.6.11" 31 | }, 32 | "devDependencies": { 33 | "@types/jest": "^24.0.23", 34 | "@typescript-eslint/parser": "^2.12.0", 35 | "bili": "^4.8.1", 36 | "eslint": "^6.7.2", 37 | "eslint-config-standard": "^14.1.0", 38 | "eslint-plugin-import": "^2.19.1", 39 | "eslint-plugin-node": "^10.0.0", 40 | "eslint-plugin-promise": "^4.2.1", 41 | "eslint-plugin-standard": "^4.0.1", 42 | "jest": "^24.9.0", 43 | "rollup-plugin-typescript2": "^0.21.0", 44 | "ts-jest": "^24.2.0", 45 | "typescript": "^3.7.3", 46 | "vuepress": "^1.2.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { VueReactiveStore } from './store' 2 | 3 | export { VueReactiveStore } 4 | export default VueReactiveStore 5 | -------------------------------------------------------------------------------- /src/plugins/devtools.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import { VRSState, VRSStore, VRSPlugin } from '../typings' 3 | 4 | // eslint-disable-next-line no-nested-ternary 5 | const target = typeof window !== 'undefined' 6 | ? window 7 | : typeof global !== 'undefined' 8 | ? global 9 | : {} 10 | // @ts-ignore 11 | const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__ 12 | 13 | /** 14 | * This interface is here to 'copy' the vuex / vue-devtools communication 15 | * It help VRS to be 'time travel' compatible, 16 | * and tell vue-devtools all mutations in vuex tab 17 | */ 18 | interface VRSStoreForDevtools extends VRSStore { 19 | // getters: VRSComputed | undefined; 20 | _mutations: { [index: string]: Function } 21 | _devtoolHook: Function 22 | flushStoreModules: Function 23 | registerModule: Function 24 | unregisterModule: Function 25 | replaceState: Function 26 | } 27 | 28 | /** 29 | * This object allows us to store a state 30 | * * recordEvents allow devtools plugin communicate to vue-devtools 31 | */ 32 | const VRSDevtoolsState = { 33 | recordEvents: true 34 | } 35 | 36 | /** 37 | * VRS Plugin for vue-devtools / vuex tab 38 | * Try to imitate the communication between vuex and vue-devtools 39 | * 40 | * In VueX, each mutation is forwarded to vue-devtools, 41 | * and time-travel is get by 'replaying' each mutation on the store's base state 42 | * 43 | * So we do the same for VRS 44 | * 45 | * @param vrsStore 46 | */ 47 | const vrsPluginDevtools = (vrsStore: VRSStoreForDevtools): VRSPlugin => { 48 | if (!devtoolHook) { 49 | console.warn('[vrs-plugin-devtools] vue-devtools is not installed. Please install before trying to use vrs-plugin-devtools') 50 | return {} 51 | } 52 | 53 | vrsStore._mutations = {} 54 | vrsStore._devtoolHook = devtoolHook 55 | 56 | /** 57 | * These three methods are required, 58 | * but for my current knowledge, 59 | * it seems useless for VRS 60 | */ 61 | vrsStore.flushStoreModules = () => { 62 | console.warn('[vrs-plugin-devtools]flush vrsStore modules is not implemented (asked by vue-devtools)') 63 | } 64 | vrsStore.registerModule = (module: string) => { 65 | console.warn(`[vrs-plugin-devtools]registerModule is not implemented (asked by vue-devtools for module '${module}')`) 66 | } 67 | vrsStore.unregisterModule = (module: string) => { 68 | console.warn(`[vrs-plugin-devtools]unregisterModule is not implemented (asked by vue-devtools for module '${module}')`) 69 | } 70 | 71 | /** 72 | * The replaceState method is called 73 | * after vue-devtools get the 'final' state 74 | * after replaying all mutations to the 'active state' 75 | */ 76 | function replaceState (targetStore: VRSStore, targetState: VRSState) { 77 | if (!targetState) { 78 | console.warn('[vrs-plugin-devtools] state is null. can\'t replace a state with null') 79 | return 80 | } 81 | Object.keys(targetState).forEach(k => { 82 | if (targetStore.modules && targetStore.modules[k]) return replaceState(targetStore.modules[k], targetState[k]) 83 | targetStore.state![k] = targetState[k] 84 | }) 85 | } 86 | 87 | vrsStore.replaceState = (targetState: VRSState) => { 88 | console.info('[vrs-plugin-devtools] replacing state... (asked by vue-devtools) ', JSON.parse(JSON.stringify(targetState)), vrsStore.name) 89 | console.info('[vrs-plugin-devtools] stop recording mutations', vrsStore.name) 90 | VRSDevtoolsState.recordEvents = false 91 | replaceState(vrsStore, targetState) 92 | VRSDevtoolsState.recordEvents = true 93 | console.info('[vrs-plugin-devtools] start recording mutations', vrsStore.name) 94 | } 95 | 96 | /** 97 | * We react to the `vuex:travel-to-state` event of vue-devtools 98 | */ 99 | devtoolHook.on('vuex:travel-to-state', (targetState: VRSState) => { 100 | console.info('[vrs-plugin-devtools] travel to state... (asked by vue-devtools) ', targetState) 101 | vrsStore.replaceState(targetState) 102 | }) 103 | 104 | // we emit the first event to vue-devtools, base state of the store 105 | devtoolHook.emit('vuex:init', vrsStore) 106 | 107 | /** 108 | * The plugin is only for state mutations 109 | * We could imagine in the near future 110 | * emit some events for actions trigerred and computed properties recomputed 111 | */ 112 | return { 113 | state: { 114 | after (rootStore, stateProperty, newValue) { 115 | // we emit events only if it's not disabled 116 | if (!VRSDevtoolsState.recordEvents) return 117 | const jsonStoreState = JSON.parse(JSON.stringify(rootStore.state)) 118 | 119 | /** 120 | * In VueX, there is an array of mutations in `_mutations` property 121 | * Not in vue-reactive-store 122 | * This code is here to mimic the array, 123 | * so vue-devtools could trigger every mutation 124 | * on the initial store it knows, thanks to the `vue:init` event 125 | */ 126 | vrsStore._mutations[stateProperty] = (payload: any) => { 127 | const arrayPaths = stateProperty.split('.') 128 | // storeName.state.property => 3 elements, it's a property 129 | if (arrayPaths.length < 3) return 130 | if (arrayPaths[0] !== vrsStore.name) return 131 | if (arrayPaths[1] !== 'state') return 132 | let currentStateObject = vrsStore.state || {} 133 | let targetStateObject = null 134 | let i = 2 135 | for (i; i < arrayPaths.length; i++) { 136 | currentStateObject = currentStateObject![arrayPaths[i]] 137 | if (!currentStateObject) break 138 | if (i === arrayPaths.length - 1) { 139 | targetStateObject = currentStateObject 140 | } 141 | } 142 | if (targetStateObject === currentStateObject) { 143 | targetStateObject = payload 144 | } 145 | } 146 | devtoolHook.emit('vuex:mutation', { 147 | type: stateProperty, 148 | payload: newValue 149 | }, jsonStoreState) 150 | } 151 | } 152 | } 153 | } 154 | 155 | export default vrsPluginDevtools 156 | -------------------------------------------------------------------------------- /src/plugins/logger.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { VRSPlugin, VRSState, VRSComputed, VRSStore, VRSPluginFunction } from '../typings' 3 | 4 | interface VRSPluginLogger extends VRSPlugin { 5 | previousValues: { 6 | state: VRSState, 7 | computed: VRSComputed 8 | } 9 | } 10 | 11 | interface VRSPluginLoggerOptions { 12 | state: boolean, 13 | computed: boolean, 14 | actions: boolean 15 | } 16 | 17 | const loggerPlugin = (options: VRSPluginLoggerOptions = { 18 | state: true, 19 | computed: true, 20 | actions: true 21 | }): VRSPluginFunction => (store: VRSStore): VRSPluginLogger => { 22 | const previousValues: { 23 | state: VRSState, 24 | computed: VRSComputed 25 | } = { 26 | state: {}, 27 | computed: {} 28 | } 29 | return { 30 | previousValues: { 31 | state: {}, 32 | computed: {} 33 | }, 34 | state: { 35 | after (store, stateProperty, newValue, oldValue) { 36 | const message = `${stateProperty} updated` 37 | previousValues.state = JSON.parse(JSON.stringify(store.state)) 38 | previousValues.computed = JSON.parse(JSON.stringify(store.computed)) 39 | if (options.state) { 40 | const newValueDisplay = typeof newValue === 'object' ? JSON.parse(JSON.stringify(newValue)) : newValue 41 | let oldValueDisplay = {} 42 | if (previousValues.state[`${store.name}.${stateProperty}`]) { 43 | oldValueDisplay = previousValues.state[`${store.name}.${stateProperty}`] 44 | } else { 45 | oldValueDisplay = typeof oldValue === 'object' ? JSON.parse(JSON.stringify(oldValue)) : oldValue 46 | } 47 | previousValues.state[`${store.name}.${stateProperty}`] = typeof newValue === 'object' ? JSON.parse(JSON.stringify(newValue)) : newValue 48 | console.groupCollapsed(message) 49 | console.log('%cprevious value', 'color: blue', oldValueDisplay) 50 | console.log('%cnext value', 'color: green', newValueDisplay) 51 | console.groupEnd() 52 | } else { 53 | console.info(`%c${message}`, 'color: green') 54 | } 55 | } 56 | }, 57 | computed: { 58 | after (store, computedProperty, newValue, oldValue) { 59 | const message = `${computedProperty} updated` 60 | previousValues.state = JSON.parse(JSON.stringify(store.state)) 61 | previousValues.computed = JSON.parse(JSON.stringify(store.computed)) 62 | if (options.computed) { 63 | const newValueDisplay = typeof newValue === 'object' ? JSON.parse(JSON.stringify(newValue)) : newValue 64 | const oldValueDisplay = typeof oldValue === 'object' ? JSON.parse(JSON.stringify(oldValue)) : oldValue 65 | console.groupCollapsed(message) 66 | console.log('computed from > to') 67 | console.log(oldValueDisplay) 68 | console.log(newValueDisplay) 69 | console.groupEnd() 70 | } else { 71 | console.info(`%c${message}`, 'color: green') 72 | } 73 | } 74 | }, 75 | actions: { 76 | before (store, actionName, wrapperId) { 77 | const message = `${actionName} trigerred [${wrapperId}]` 78 | previousValues.state = JSON.parse(JSON.stringify(store.state)) 79 | previousValues.computed = JSON.parse(JSON.stringify(store.computed)) 80 | if (options.actions) { 81 | console.groupCollapsed(message) 82 | console.groupCollapsed('trace') 83 | console.trace() 84 | console.groupEnd() 85 | console.groupEnd() 86 | } else { 87 | console.info(`%c${message}`, 'color: blue') 88 | } 89 | }, 90 | after (store, actionName, wrapperId) { 91 | const message = `${actionName} finished [${wrapperId}]` 92 | previousValues.state = JSON.parse(JSON.stringify(store.state)) 93 | previousValues.computed = JSON.parse(JSON.stringify(store.computed)) 94 | if (options.actions) { 95 | console.groupCollapsed(message) 96 | console.groupCollapsed('trace') 97 | console.trace() 98 | console.groupEnd() 99 | console.groupEnd() 100 | } else { 101 | console.info(`%c${message}`, 'color: blue') 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | export default loggerPlugin 109 | -------------------------------------------------------------------------------- /src/store.spec.ts: -------------------------------------------------------------------------------- 1 | // import 'jest'; 2 | import Vue from 'vue' 3 | import { VueReactiveStore } from './store' 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | import { VRSStore, VRSHook, VRSPlugin, VRSState } from './typings' 7 | 8 | describe('VueReactiveStore', () => { 9 | test('has a register plugin function available', () => { 10 | expect(VueReactiveStore.registerPlugin).toBeDefined() 11 | }) 12 | test('throw an error if no store is given in param', () => { 13 | expect(() => { 14 | // @ts-ignore 15 | // eslint-disable-next-line 16 | new VueReactiveStore() 17 | }).toThrow() 18 | }) 19 | test('set default values for name and state if not provided', () => { 20 | const reactiveStore = new VueReactiveStore({ }) 21 | expect(reactiveStore.name).toBe('my-store') 22 | // expect(reactiveStore.state).toEqual({}) 23 | }) 24 | test('is built with a JS Object with a state sharing the same reference than VRS Store', () => { 25 | const state = { 26 | myData: 'pouet', 27 | myData2: 'pouic' 28 | } 29 | const store = new VueReactiveStore({ 30 | name: 'my-store', 31 | state 32 | }) 33 | expect(store.name).toBe('my-store') 34 | expect(store.state).toBe(state) 35 | expect(store.computed).toStrictEqual({}) 36 | expect(store.actions).toStrictEqual({}) 37 | expect(store.plugins).toStrictEqual([]) 38 | expect(store.modules).toStrictEqual({}) 39 | }) 40 | test('can be mutated directly and the state of VRS Store is equal', () => { 41 | const state = { 42 | myData: 'pouet', 43 | myData2: 'pouic' 44 | } 45 | const store = new VueReactiveStore({ 46 | name: 'my-store', 47 | state 48 | }) 49 | state.myData = 'hello' 50 | expect(state.myData).toBe('hello') 51 | expect(store.state.myData).toBe('hello') 52 | expect(state.myData).toBe(store.state.myData) 53 | }) 54 | test('add reactive modules to the main store', () => { 55 | const module1 = { 56 | name: 'module1', 57 | state: { 58 | myData: 'myData of module1', 59 | myData2: 'myData2 of module1' 60 | }, 61 | computed: { 62 | myComputed () { 63 | return module1.state.myData + module1.state.myData2 64 | } 65 | } 66 | } 67 | const jsStore = { 68 | name: 'my-store', 69 | state: { 70 | myData: 'pouet', 71 | myData2: 'pouic' 72 | }, 73 | modules: { 74 | module1 75 | } 76 | } 77 | const store = new VueReactiveStore(jsStore) 78 | expect(store.state.module1).toBe(module1.state) 79 | }) 80 | test('throw an error when a module is named like a state prop', () => { 81 | const module1: VRSStore = { 82 | name: 'module1', 83 | state: { 84 | myData: 'myData of module1', 85 | myData2: 'myData2 of module1' 86 | } 87 | } 88 | const jsStore: VRSStore = { 89 | name: 'my-store', 90 | state: { 91 | myData: 'pouet', 92 | myData2: 'pouic' 93 | }, 94 | modules: { 95 | myData: module1 96 | } 97 | } 98 | expect(() => { 99 | const reactiveStore = new VueReactiveStore(jsStore) 100 | }).toThrow() 101 | }) 102 | 103 | /** 104 | * Hooks testing 105 | */ 106 | test('call hookWrapper for each actionHook available', async () => { 107 | const myMockAction = (param: string) => { 108 | expect(param).toBe('hello') 109 | jsStore.state!.myData = param 110 | } 111 | const jsStore: VRSStore = { 112 | name: 'my-store', 113 | state: { 114 | myData: 'pouet', 115 | myData2: 'pouic' 116 | }, 117 | actions: { 118 | myAction: myMockAction 119 | }, 120 | plugins: [{ 121 | actions: { 122 | before (store: VRSStore, funcName: string, wrapperId: string) { 123 | expect(store.name).toBe('my-store') 124 | expect(funcName).toBe('my-store.actions.myAction') 125 | expect(store.state).toStrictEqual({ 126 | myData: 'pouet', 127 | myData2: 'pouic' 128 | }) 129 | }, 130 | after (store: VRSStore, funcName: string, wrapperId: string) { 131 | expect(store.name).toBe('my-store') 132 | expect(funcName).toBe('my-store.actions.myAction') 133 | expect(store.state).toStrictEqual({ 134 | myData: 'hello', 135 | myData2: 'pouic' 136 | }) 137 | } 138 | } 139 | }] 140 | } 141 | const reactiveStore = new VueReactiveStore(jsStore) 142 | expect(jsStore.actions!.myAction).not.toBe(myMockAction) 143 | jsStore.actions!.myAction('hello') 144 | }) 145 | test('throw an error when a plugin is already registered', async () => { 146 | const plugin = { 147 | state: { 148 | after: jest.fn() 149 | } 150 | } 151 | const originalWarn = console.warn 152 | console.warn = jest.fn() 153 | VueReactiveStore.registerPlugin(plugin) 154 | VueReactiveStore.registerPlugin(plugin) 155 | expect(console.warn).toHaveBeenCalled() 156 | console.warn = originalWarn 157 | }) 158 | test('trigger a global plugin on a state (after) when a state property change', async () => { 159 | const jsStore: VRSStore = { 160 | name: 'my-store', 161 | state: { 162 | myData: 'pouet', 163 | myData2: 'pouic' 164 | } 165 | } 166 | const plugin = { 167 | state: { 168 | after: jest.fn() 169 | } 170 | } 171 | VueReactiveStore.registerPlugin(plugin) 172 | const reactiveStore = new VueReactiveStore(jsStore) 173 | jsStore.state!.myData = 'hello' 174 | await Vue.nextTick() 175 | expect(plugin.state.after).toHaveBeenCalled() 176 | expect(plugin.state.after).toHaveBeenCalledWith(reactiveStore, 'my-store.state.myData', 'hello', 'pouet') 177 | }) 178 | test('trigger a local plugin on a state (after) when a state property change', async () => { 179 | const jsStore: VRSStore = { 180 | name: 'my-store', 181 | state: { 182 | myData: 'pouet', 183 | myData2: 'pouic' 184 | }, 185 | plugins: [ 186 | { 187 | state: { 188 | after: jest.fn() 189 | } 190 | } 191 | ] 192 | } 193 | const reactiveStore = new VueReactiveStore(jsStore) 194 | jsStore.state!.myData = 'hello' 195 | await Vue.nextTick() 196 | expect((jsStore.plugins![0] as VRSPlugin).state!.after).toHaveBeenCalled() 197 | expect((jsStore.plugins![0] as VRSPlugin).state!.after).toHaveBeenCalledWith(reactiveStore, 'my-store.state.myData', 'hello', 'pouet') 198 | }) 199 | test('trigger a global hook on a computed (after) when a computed property change', async () => { 200 | const jsStore: VRSStore = { 201 | name: 'my-store', 202 | state: { 203 | myData: 'pouet', 204 | myData2: 'pouic' 205 | }, 206 | computed: { 207 | myComputed () { 208 | return jsStore.state!.myData + jsStore.state!.myData2 209 | } 210 | } 211 | } 212 | const plugin = { 213 | computed: { 214 | after: jest.fn() 215 | } 216 | } 217 | VueReactiveStore.registerPlugin(plugin) 218 | const reactiveStore = new VueReactiveStore(jsStore) 219 | jsStore.state!.myData = 'hello' 220 | await Vue.nextTick() 221 | expect(plugin.computed.after).toHaveBeenCalled() 222 | expect(plugin.computed.after).toHaveBeenCalledWith(reactiveStore, 'my-store.computed.myComputed', 'hellopouic', 'pouetpouic') 223 | }) 224 | test('trigger a local hook on a computed (after) when a computed property change', async () => { 225 | const jsStore: VRSStore = { 226 | name: 'my-store', 227 | state: { 228 | myData: 'pouet', 229 | myData2: 'pouic' 230 | }, 231 | computed: { 232 | myComputed () { 233 | return jsStore.state!.myData + jsStore.state!.myData2 234 | } 235 | }, 236 | plugins: [{ 237 | computed: { 238 | after: jest.fn() 239 | } 240 | }] 241 | } 242 | const reactiveStore = new VueReactiveStore(jsStore) 243 | jsStore.state!.myData = 'hello' 244 | await Vue.nextTick() 245 | expect((jsStore.plugins![0] as VRSPlugin).computed!.after).toHaveBeenCalled() 246 | expect((jsStore.plugins![0] as VRSPlugin).computed!.after).toHaveBeenCalledWith(reactiveStore, 'my-store.computed.myComputed', 'hellopouic', 'pouetpouic') 247 | }) 248 | test('trigger a hook with the right name of store when it s a state property of a module', async () => { 249 | const module1: VRSStore = { 250 | name: 'module1', 251 | state: { 252 | myData: 'myData of module1', 253 | myData2: 'myData2 of module1' 254 | } 255 | } 256 | const jsStore: VRSStore = { 257 | name: 'my-store', 258 | state: {}, 259 | modules: { 260 | myComputed: module1 261 | } 262 | } 263 | const plugin = { 264 | state: { 265 | after: jest.fn() 266 | } 267 | } 268 | VueReactiveStore.registerPlugin(plugin) 269 | const reactiveStore = new VueReactiveStore(jsStore) 270 | module1.state!.myData = 'hello' 271 | await Vue.nextTick() 272 | expect(plugin.state.after).toHaveBeenCalled() 273 | expect(plugin.state.after).toHaveBeenCalledWith(reactiveStore, 'my-store.modules.module1.state.myData', 'hello', 'myData of module1') 274 | }) 275 | test('trigger a watch correctly', () => { 276 | expect.assertions(2) 277 | const jsStore: VRSStore = { 278 | name: 'my-store', 279 | state: { 280 | myData: 'myData of module1' 281 | }, 282 | watch: { 283 | myData (newValue, oldValue) { 284 | expect(oldValue).toBe('myData of module1') 285 | expect(newValue).toBe('hello') 286 | } 287 | } 288 | } 289 | const reactiveStore = new VueReactiveStore(jsStore) 290 | jsStore.state!.myData = 'hello' 291 | }) 292 | }) 293 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import Vue, { ComputedOptions, WatchOptionsWithHandler, WatchHandler } from 'vue' 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | import { VRSStore, VRSPlugin, VRSPluginFunction } from './typings' 6 | 7 | import { hookWrapper } from './wrapper' 8 | 9 | /** 10 | * Vue Reactive Store, 11 | * based on VueJS reactivity system. 12 | * Inspired by VueX and Vue.js instance. 13 | */ 14 | export class VueReactiveStore implements VRSStore { 15 | /** 16 | * Global plugins, called each time : 17 | * * an action is trigerred (before / after hooks) 18 | * * a property of the state is mutated (after hook) 19 | * * a computed property has been recomputed (after hook) 20 | * * a prop property has changed (after hook, mutated out of the store himself) 21 | * * a watch triger has been trigerred (after hook) 22 | */ 23 | private static globalPlugins: VRSPlugin[] = []; 24 | 25 | static registerPlugin (plugin: VRSPlugin) { 26 | if (VueReactiveStore.globalPlugins.indexOf(plugin) === -1) { 27 | VueReactiveStore.globalPlugins.push(plugin) 28 | } else { 29 | console.warn('You\'re trying to add a plugin already registered.') 30 | } 31 | } 32 | 33 | /** 34 | * local Vue instance, 35 | * to use reactivity system of Vue.js 36 | */ 37 | private _vm: any; 38 | 39 | name = ''; 40 | state: { 41 | [name: string]: any, 42 | } = {} 43 | 44 | actions: { 45 | [name: string]: Function 46 | } = {} 47 | 48 | computed: { 49 | [name: string]: (() => any) | ComputedOptions 50 | } = {} 51 | 52 | watch: Record | WatchHandler> = {} 53 | plugins: VRSPlugin[] = [] 54 | modules: { 55 | [name: string]: VRSStore 56 | } = {} 57 | 58 | private subStores: { 59 | [name: string]: VueReactiveStore 60 | } = {} 61 | 62 | private rootStore: VRSStore 63 | 64 | /** 65 | * Reactive store based on VueJS reactivity system 66 | * 67 | * @param {VRSStore} store 68 | * The store, composed of : 69 | * * name 70 | * * state 71 | * * computed properties 72 | * * actions potentially async 73 | * * watchers 74 | * * modules, aka sub-stores (wip) 75 | * * plugins that could be trigerred before / after store evolution 76 | * @param {VRSStore} rootStore 77 | * Used only for internal purpose (plugins), 78 | * reference to the root store. 79 | */ 80 | constructor (store: VRSStore, rootStore?: VRSStore) { 81 | if (!store) throw new Error('Please provide a store to VueReactiveStore') 82 | this.name = store.name || 'my-store' 83 | this.state = store.state || {} 84 | this.computed = store.computed || {} 85 | this.actions = store.actions || {} 86 | this.watch = store.watch || {} 87 | this.plugins = [] 88 | if (store.plugins) { 89 | store.plugins.forEach(p => { 90 | if (typeof p === 'function') { 91 | this.plugins.push((p)(this)) 92 | } else { 93 | this.plugins.push(p) 94 | } 95 | }) 96 | } 97 | this.modules = store.modules || {} 98 | this.rootStore = rootStore || this 99 | 100 | // check if each module doesn't exist in store state 101 | // or in a computed property 102 | // to correctly namespace them 103 | Object.keys(this.modules).forEach((moduleName) => { 104 | if (this.state[moduleName]) { 105 | const errorMessage = ` 106 | You're trying to add a module which its name already exist as a state property. 107 | Please rename your module or your state property. 108 | (Store ${this.name}, property/module ${moduleName}) 109 | ` 110 | throw new Error(errorMessage) 111 | } 112 | // if (this.computed[moduleName]) { 113 | // const errorMessage = ` 114 | // You're trying to add a module which its name already exist as a computed property. 115 | // Please rename your module or your computed property. 116 | // (Store ${this.name}, computed/module ${moduleName}) 117 | // ` 118 | // throw new Error(errorMessage) 119 | // } 120 | this.subStores[moduleName] = new VueReactiveStore({ 121 | ...this.modules[moduleName], 122 | name: this.name + '.modules.' + (this.modules[moduleName].name || moduleName), 123 | plugins: (this.modules[moduleName].plugins || []).concat(this.plugins) 124 | }, this.rootStore) 125 | }) 126 | 127 | // group all actions hooks available, from global and local plugins 128 | const actionsHooks = VueReactiveStore.globalPlugins.map((hook: VRSPlugin) => ({ 129 | ...hook.actions 130 | })).concat(this.plugins.map((hook: VRSPlugin) => ({ 131 | ...hook.actions 132 | }))) 133 | 134 | Object.keys(this.actions).forEach((key) => { 135 | this.actions[key] = hookWrapper( 136 | this.rootStore, 137 | this.name + '.actions.' + key, 138 | this.actions[key], 139 | actionsHooks 140 | ) 141 | }) 142 | 143 | // create a Vue instance 144 | // to use the VueJS reactivity system 145 | this._vm = new Vue({ 146 | data: () => this.state, 147 | computed: store.computed, 148 | watch: store.watch 149 | }) 150 | 151 | // now we can replace the initial store 152 | // with _vm attributes 153 | store.state = this._vm.$data 154 | 155 | // listen to state mutations of current store 156 | // doesn't work for modules because they will be added after 157 | // knowing each modules has been transformed in VRS 158 | Object.keys(this.state).forEach((key) => { 159 | this._vm.$watch(key, (newValue: any, oldValue: any) => { 160 | // call global hooks in order 161 | VueReactiveStore.globalPlugins.forEach((hook) => { 162 | hook.state && hook.state.after && hook.state.after(this.rootStore, this.name + '.state.' + key, newValue, oldValue) 163 | }) 164 | this.plugins.forEach((hook) => { 165 | hook.state && hook.state.after && hook.state.after(this.rootStore, this.name + '.state.' + key, newValue, oldValue) 166 | }) 167 | }, { 168 | deep: true 169 | }) 170 | }) 171 | 172 | // listen to computed properties recomputed 173 | // idem, doesn't work for modules 174 | Object.keys(this.computed).forEach((key) => { 175 | this._vm.$watch(key, (newValue: any, oldValue: any) => { 176 | // call global hooks in order 177 | VueReactiveStore.globalPlugins.forEach((hook) => { 178 | hook.computed && hook.computed.after && hook.computed.after(this.rootStore, this.name + '.computed.' + key, newValue, oldValue) 179 | }) 180 | this.plugins.forEach((hook) => { 181 | hook.computed && hook.computed.after && hook.computed.after(this.rootStore, this.name + '.computed.' + key, newValue, oldValue) 182 | }) 183 | }, { 184 | deep: true 185 | }) 186 | }) 187 | 188 | /** 189 | * We add each module state/computed 190 | * in the store where they are added 191 | */ 192 | Object.keys(this.subStores).forEach((moduleName) => { 193 | this.state[moduleName] = this.subStores[moduleName].state 194 | // this won't work, because in computed we have to store functions, 195 | // but here we're trying to store Objects... ? so we can't nest modules.computed 196 | // this.computed[moduleName] = this.subStores[moduleName].computed 197 | }) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/typings.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { ComputedOptions, WatchOptionsWithHandler, WatchHandler } from 'vue' 3 | 4 | interface VRSHookFunction { 5 | (store: VRSStore, funcName: string, wrapperId: string): any 6 | } 7 | 8 | interface VRSHookFunctionOldNewValues { 9 | (store: VRSStore, funcName: string, newValue: any, oldValue: any): any 10 | } 11 | 12 | export interface VRSHook { 13 | after?: VRSHookFunction, 14 | before?: VRSHookFunction 15 | } 16 | 17 | interface VRSHookAfterOnly { 18 | after: VRSHookFunctionOldNewValues 19 | } 20 | 21 | /** 22 | * Plugin for Vue Reactive Store, 23 | * composed of hooks trigerred : 24 | * * before / after each actions 25 | * * after state is mutated 26 | * * after computed properties are recomputed 27 | * * before / after watchers are trigerred 28 | * 29 | * Each hook function takes 3 to 4 params : 30 | * * name of the reactive store 31 | * * key, = name of the action, watcher, state or computed property 32 | * * currentState (actions / watchers) / initialValue (state / computed) 33 | * * finalValue (state / computed) 34 | */ 35 | export interface VRSPlugin { 36 | actions?: VRSHook, 37 | state?: VRSHookAfterOnly, 38 | computed?: VRSHookAfterOnly, 39 | watch?: VRSHook, 40 | } 41 | 42 | export interface VRSPluginFunction { 43 | (store: VRSStore): VRSPlugin 44 | } 45 | 46 | export interface VRSState { 47 | [name: string]: any, 48 | } 49 | 50 | export interface VRSComputed { 51 | [name: string]: (() => any) | ComputedOptions 52 | } 53 | 54 | interface VRSActions { 55 | [name: string]: Function, 56 | } 57 | 58 | /** 59 | * Store of VRS 60 | */ 61 | export interface VRSStore { 62 | name?: string, 63 | state?: VRSState, 64 | actions?: VRSActions, 65 | computed?: VRSComputed, 66 | // props?: Object, 67 | watch?: Record | WatchHandler>, 68 | plugins?: Array, 69 | modules?: { 70 | [name: string]: VRSStore 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /src/wrapper.spec.ts: -------------------------------------------------------------------------------- 1 | // import 'jest'; 2 | import { hookWrapper } from './wrapper' 3 | // eslint-disable-next-line no-unused-vars 4 | import { VRSState, VRSStore } from './typings' 5 | 6 | describe('hookWrapper', () => { 7 | test('call the initial function without modifying params', () => { 8 | expect.assertions(2) 9 | const state = { 10 | myData: 'pouet', 11 | myData2: 'pouic' 12 | } 13 | const functionWrapped = hookWrapper({ 14 | name: 'store name', 15 | state 16 | }, 17 | 'functionName', 18 | function (newMyData: string, newMyData2: string) { 19 | expect(newMyData).toBe('hi') 20 | expect(newMyData2).toBe('ho') 21 | state.myData = newMyData 22 | state.myData2 = newMyData2 23 | }, 24 | [] 25 | ) 26 | functionWrapped('hi', 'ho') 27 | }) 28 | test('call before/after hooks with right params', () => { 29 | expect.assertions(9) 30 | let wrapperIdBeforeHook = 'this is a wrapper id' 31 | const state = { 32 | myData: 'pouet', 33 | myData2: 'pouic' 34 | } 35 | const functionWrapped = hookWrapper({ 36 | name: 'store name', 37 | state 38 | }, 39 | 'functionName', 40 | function (newMyData: string, newMyData2: string) { 41 | state.myData = newMyData 42 | state.myData2 = newMyData2 43 | }, 44 | [ 45 | { 46 | before (store: VRSStore, funcName: string, wrapperId: string) { 47 | expect(store.name).toBe('store name') 48 | expect(store.state!.myData).toBe('pouet') 49 | expect(store.state!.myData2).toBe('pouic') 50 | expect(funcName).toBe('functionName') 51 | wrapperIdBeforeHook = wrapperId 52 | }, 53 | after (store: VRSStore, funcName: string, wrapperId: string) { 54 | expect(store.name).toBe('store name') 55 | expect(store.state!.myData).toBe('hi') 56 | expect(store.state!.myData2).toBe('ho') 57 | expect(funcName).toBe('functionName') 58 | expect(wrapperId).toBe(wrapperIdBeforeHook) 59 | } 60 | } 61 | ] 62 | ) 63 | functionWrapped('hi', 'ho') 64 | }) 65 | test('call before/after hooks with right params when action is async', async () => { 66 | expect.assertions(9) 67 | let wrapperIdBeforeHook = '' 68 | const state = { 69 | myData: 'pouet', 70 | myData2: 'pouic' 71 | } 72 | const functionWrapped = hookWrapper({ 73 | name: 'store name', 74 | state 75 | }, 76 | 'functionName', 77 | function (newMyData: string, newMyData2: string) { 78 | return new Promise((resolve) => { 79 | state.myData = newMyData 80 | state.myData2 = newMyData2 81 | setTimeout(resolve, 1000) 82 | }) 83 | }, 84 | [ 85 | { 86 | before (store: VRSStore, funcName: string, wrapperId: string) { 87 | expect(store.name).toBe('store name') 88 | expect(store.state!.myData).toBe('pouet') 89 | expect(store.state!.myData2).toBe('pouic') 90 | expect(funcName).toBe('functionName') 91 | wrapperIdBeforeHook = wrapperId 92 | }, 93 | after (store: VRSStore, funcName: string, wrapperId: string) { 94 | expect(store.name).toBe('store name') 95 | expect(store.state!.myData).toBe('hi') 96 | expect(store.state!.myData2).toBe('ho') 97 | expect(funcName).toBe('functionName') 98 | expect(wrapperId).toBe(wrapperIdBeforeHook) 99 | } 100 | } 101 | ] 102 | ) 103 | await functionWrapped('hi', 'ho') 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /src/wrapper.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { VRSHook, VRSStore } from './typings' 5 | 6 | function uuidv4 () { 7 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 8 | var r = Math.random() * 16 | 0; var v = c === 'x' ? r : (r & 0x3 | 0x8) 9 | return v.toString(16) 10 | }) 11 | } 12 | 13 | /** 14 | * Wrapper for action, 15 | * allow for trigger hooks 16 | */ 17 | export const hookWrapper = ( 18 | store: VRSStore, 19 | key: string, 20 | func: Function, 21 | hooks: VRSHook[] 22 | ) => async (...args: any[]) => { 23 | const wrapperId = uuidv4() 24 | // wait for initial mutation of store 25 | // await Vue.nextTick() 26 | 27 | return new Promise((resolve, reject) => { 28 | // call all before hooks 29 | // and memorize result to pass it to the after hook 30 | hooks.forEach(hook => { 31 | hook.before && hook.before(store, key, wrapperId) 32 | }) 33 | 34 | // we call the initial function with parameters 35 | // if it's a promise, we will use await on it 36 | // const funcResult: Promise<{}>|{} = func(...args) 37 | // if (funcResult instanceof Promise) { 38 | // await funcResult 39 | // } 40 | 41 | // we wait for the next DOM update (and so the state mutations) 42 | // await Vue.nextTick() 43 | // we call the initial function with parameters 44 | const fnRes = func(...args) 45 | if (fnRes && typeof fnRes.then === 'function') { 46 | // it's probably a promise, we work async 47 | fnRes.then(async (result: any) => { 48 | // we wait for the next DOM update (and so the state mutations) 49 | await Vue.nextTick() 50 | 51 | // call all after hooks 52 | hooks.forEach(hook => { 53 | hook.after && hook.after(store, key, wrapperId) 54 | }) 55 | resolve(result) 56 | }).catch(async (error: Error) => { 57 | // we wait for the next DOM update (and so the state mutations) 58 | await Vue.nextTick() 59 | 60 | // call all after hooks 61 | hooks.forEach(hook => { 62 | hook.after && hook.after(store, key, wrapperId) 63 | }) 64 | reject(error) 65 | }) 66 | } else { 67 | resolve(fnRes) 68 | } 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["es2015", "dom"], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | "declarationDir": "./types", /* Output directory for generated declaration files. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist/", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | }, 60 | "include": [ 61 | "src", 62 | ], 63 | "exclude": [ 64 | "**/*.spec.ts", 65 | ] 66 | } 67 | --------------------------------------------------------------------------------