├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── class ├── index.d.ts └── index.js ├── helpers ├── index.d.ts └── index.js ├── index.d.ts ├── index.js ├── jsconfig.json ├── package.json ├── test ├── unit │ ├── class.test.js │ ├── helpers.test.js │ └── index.test.js └── utils.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | yarn-error.log 3 | 4 | .cache/ 5 | .parcel-cache/ 6 | node_modules/ 7 | dist/ 8 | coverage/ 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | 3 | npm-debug.log 4 | yarn-error.log 5 | package-lock.json 6 | yarn.lock 7 | 8 | .travis.yml 9 | 10 | node_modules/ 11 | example/ 12 | coverage/ 13 | .cache/ 14 | dist/ 15 | test/ 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - node 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## 3.0.0-beta.2 6 | 7 | ### Breaking Changes 8 | 9 | #### Installation process has changed 10 | 11 | To install StoreonVue to Vue app instance, pass the store instance to `createStoreonPlugin` function. 12 | 13 | ```js 14 | import { createApp } from 'vue' 15 | import { createStoreonPlugin } from '@storeon/vue' 16 | import App from './App.vue' 17 | import { store } from './store' 18 | 19 | const app = createApp(App) 20 | 21 | app.use(createStoreonPlugin(store)) 22 | 23 | app.mount('#app') 24 | ``` 25 | 26 | ## 2.0.0 27 | - Drop support for storeon < v3 and node < v10 ([a9a27c](https://github.com/storeon/vue/commit/a9a27c3ca76e678b11748eb7a56fc91b750d0d3c)) 28 | 29 | ## 1.1.0 30 | - Add support for Composition API ([6e77e0](https://github.com/storeon/vue/commit/3e65191fa4d8586c1197a64852b0cb39d6fe6fdf)) 31 | 32 | ## 1.0.0 33 | - Add support for Class Components ([6e77e0](https://github.com/storeon/vue/commit/6e77e000118f52aa8506a70e583f19a192080d63)) 34 | - Add helper functions ([532121](https://github.com/storeon/vue/commit/532121764a7fa5e61d37e3f8325eccfea4deebf9)) 35 | 36 | ## 0.5.0 37 | - Use dual-publish for ESModules ([a1a76c](https://github.com/storeon/vue/commit/a1a76c0b3c7197d661e133f64cc4ea770e50c247)) 38 | 39 | ## 0.4.0 40 | - Move to Storeon org ([7bd0a9](https://github.com/storeon/vue/commit/7bd0a95f31e018b3cefcf1a2fd6e769db13d29f7)) 41 | - Optimise perfomance ([bf23f3](https://github.com/storeon/vue/commit/bf23f3b7809ca815a839bb8350acce0b457ad036)) 42 | 43 | ## 0.3.0 44 | - Use named export ([3fd201](https://github.com/storeon/vue/commit/3fd201ea0e199f82fccc0df5f733fa18f16f463a)) 45 | 46 | ## 0.2.2 47 | - Remove unnecessary type declarations ([d67d76](https://github.com/storeon/vue/commit/d67d765e1d09470b4260cfd9be20b61a6f4d2143)) 48 | 49 | ## 0.2.1 50 | - Add type definitions ([3bd245](https://github.com/storeon/vue/commit/3bd245319cc3c7f76d924f322d814f5fba683434)) 51 | 52 | ## 0.2.0 53 | - Simplify API ([773929](https://github.com/storeon/vue/commit/773929714f27dd0ca78ed72b6f8ade6d4bde5f37)) 54 | 55 | ## 0.1.0 56 | - change api to avoid component re-render ([2b9a97](https://github.com/storeon/vue/commit/2b9a9750763bfdab7585851500defd512f3a8422)) 57 | 58 | ## 0.0.1 59 | 60 | - Initial release. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 distolma 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 | # Storeon Vue 2 | 3 | [![npm version](https://badge.fury.io/js/%40storeon%2Fvue.svg)](https://www.npmjs.com/package/@storeon/vue) 4 | [![Build Status](https://travis-ci.org/storeon/vue.svg?branch=master)](https://travis-ci.org/storeon/vue) 5 | 6 | Storeon logo by Anton Lovchikov 7 | 8 | **This is the Vue 3 compatible version of the package. For the Vue 2 support, see the [2.0 branch](https://github.com/storeon/vue/tree/2.0).** 9 | 10 | [Storeon] is a tiny event-based Redux-like state manager without dependencies. `@storeon/vue` package helps to connect store with [Vue] to provide a better performance and developer experience while remaining so tiny. 11 | 12 | - **Size**. 160 bytes (+ Storeon itself) instead of ~3kB of [Vuex] (minified and gzipped). 13 | - **Ecosystem**. Many additional [tools] can be combined with a store. 14 | - **Speed**. It tracks what parts of state were changed and re-renders only components based on the changes. 15 | 16 | Read more about Storeon [article]. 17 | 18 | [vue]: https://github.com/vuejs/vue 19 | [vuex]: https://github.com/vuejs/vuex 20 | [storeon]: https://github.com/storeon/storeon 21 | [tools]: https://github.com/storeon/storeon#tools 22 | [vue]: https://github.com/vuejs/vue 23 | [size limit]: https://github.com/ai/size-limit 24 | [demo]: https://codesandbox.io/s/throbbing-sunset-x27qc 25 | [article]: https://evilmartians.com/chronicles/storeon-redux-in-173-bytes 26 | [vscode]: https://github.com/microsoft/vscode 27 | [vscodium]: https://github.com/VSCodium/vscodium 28 | 29 | ## Install 30 | 31 | ```sh 32 | npm install @storeon/vue -S 33 | ``` 34 | or 35 | ```sh 36 | yarn add @storeon/vue 37 | ``` 38 | 39 | ## How to use ([Demo]) 40 | 41 | Create a store with `storeon` as you do it usually. You must explicitly install plugin `@storeon/vue` via `app.use()`. 42 | 43 | #### `store.js` 44 | 45 | ```js 46 | import { createStoreon } from 'storeon' 47 | 48 | let counter = store => { 49 | store.on('@init', () => ({ count: 0 })) 50 | store.on('inc', ({ count }) => ({ count: count + 1 })) 51 | store.on('dec', ({ count }) => ({ count: count - 1 })) 52 | store.on('incBy', ({ count }, amount) => ({ count: count + amount })) 53 | } 54 | 55 | export const store = createStoreon([counter]) 56 | ``` 57 | 58 | #### `index.js` 59 | 60 | Library provides a mechanism to "inject" the store into all child components from the root component with the `store` option: 61 | 62 | ```js 63 | import { createApp } from 'vue' 64 | import { createStoreonPlugin } from '@storeon/vue' 65 | import App from './App.vue' 66 | import { store } from './store' 67 | 68 | const app = createApp(App) 69 | 70 | app.use(createStoreonPlugin(store)) 71 | 72 | app.mount('#app') 73 | ``` 74 | 75 | By providing the `store` option to the root instance, the store will be injected 76 | into all child components of the root and will be available on them as `this.$storeon`. 77 | 78 | #### `App.vue` 79 | 80 | ```html 81 | 88 | 89 | 101 | ``` 102 | 103 | Or use Composition API with `useStoreon` hook to get state and dispatch function 104 | 105 | #### `App.vue` 106 | 107 | ```html 108 | 115 | 116 | 136 | ``` 137 | 138 | ### The `mapState` Helper 139 | 140 | When a component needs to make use of multiple store state properties, declaring all these computed properties can get repetitive and verbose. To deal with this we can make use of the `mapState` helper which generates computed getter functions for us, saving us some keystrokes: 141 | 142 | ```js 143 | import { mapState } from '@storeon/vue/helpers' 144 | 145 | export default { 146 | computed: mapState({ 147 | // arrow functions can make the code very succinct! 148 | count: state => state.count, 149 | // passing the string value 'count' is same as `state => state.count` 150 | countAlias: 'count', 151 | // to access local state with `this`, a normal function must be used 152 | countPlusLocalState (state) { 153 | return state.count + this.localCount 154 | } 155 | }) 156 | } 157 | ``` 158 | 159 | We can also pass a string array to `mapState` when the name of a mapped computed property is the same as a state sub-tree name. 160 | 161 | ```js 162 | import { mapState } from '@storeon/vue/helpers' 163 | 164 | export default { 165 | computed: mapState([ 166 | // map this.count to storeon.state.count 167 | 'count' 168 | ]) 169 | } 170 | ``` 171 | 172 | ### The `mapDispatch` Helper 173 | 174 | You can dispatch actions in components with `this.$storeon.dispatch('xxx')`, or use the `mapDispatch` helper which maps component methods to `store.dispatch` calls: 175 | 176 | ```js 177 | import { mapDispatch } from '@storeon/vue/helpers' 178 | 179 | export default { 180 | methods: { 181 | ...mapDispatch([ 182 | // map `this.inc()` to `this.$storeon.dispatch('inc')` 183 | 'inc', 184 | // map `this.incBy(amount)` to `this.$storeon.dispatch('incBy', amount)` 185 | 'incBy' 186 | ]), 187 | ...mapDispatch({ 188 | // map `this.add()` to `this.$storeon.dispatch('inc')` 189 | add: 'inc' 190 | }) 191 | } 192 | } 193 | 194 | ``` 195 | 196 | ## Using with Class Components 197 | 198 | If you would like to write as more class-like style, use decorators from `@storeon/vue/class` 199 | 200 | ```js 201 | import { Vue } from 'vue-class-component' 202 | import { State, Dispatch } from '@storeon/vue/class' 203 | 204 | export default class extends Vue { 205 | @State count 206 | @Dispatch('inc') inc 207 | @Dispatch('dec') dec 208 | } 209 | ``` 210 | 211 | ## Using with TypeScript 212 | 213 | Plugin adds to Vue’s global/instance properties and component options. In these cases, type declarations are needed to make plugins compile in TypeScript. We can declare an instance property `$storeon` with type `StoreonStore`. You can also declare a component options `store`: 214 | 215 | #### `typing.d.ts` 216 | 217 | ```ts 218 | import { ComponentCustomProperties } from 'vue' 219 | import { StoreonStore } from 'storeon' 220 | import { StoreonVueStore } from '@storeon/vue' 221 | import { State, Events } from './store' 222 | 223 | declare module '@vue/runtime-core' { 224 | interface ComponentCustomProperties { 225 | $storeon: StoreonVueStore 226 | } 227 | } 228 | ``` 229 | 230 | To let TypeScript properly infer types inside Vue component options, you need to define components with `defineComponent` function: 231 | 232 | ```diff 233 | -export default { 234 | +export default defineComponent({ 235 | methods: { 236 | inc() { 237 | this.$storeon.dispatch('inc') 238 | } 239 | } 240 | }; 241 | ``` 242 | 243 | :warning: To enable type checking in your template use this flag in the `settings.json` of your [VSCode] or [VSCodium] with `Vetur` plugin. For more information see [Vetur documentation](https://vuejs.github.io/vetur/interpolation.html#generic-language-features) 244 | 245 | ```json 246 | { 247 | "vetur.experimental.templateInterpolationService": true 248 | } 249 | ``` 250 | ## TODO 251 | - Add examples 252 | -------------------------------------------------------------------------------- /class/index.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | declare type StateTransformer = (state: any) => any; 4 | declare type VuexDecorator = (proto: V, key: string) => void; 5 | interface BindingHelper { 6 | (proto: V, key: string): void; 7 | (type: string): VuexDecorator; 8 | } 9 | interface StateBindingHelper extends BindingHelper { 10 | (type: StateTransformer): VuexDecorator; 11 | } 12 | 13 | export declare const State: StateBindingHelper; 14 | export declare const Dispatch: BindingHelper; 15 | -------------------------------------------------------------------------------- /class/index.js: -------------------------------------------------------------------------------- 1 | const { createDecorator } = require('vue-class-component') 2 | 3 | const { mapDispatch, mapState } = require('../helpers') 4 | 5 | const State = createBindingHelper('computed', mapState) 6 | const Dispatch = createBindingHelper('methods', mapDispatch) 7 | 8 | module.exports = { State, Dispatch } 9 | 10 | function createBindingHelper (bindTo, mapFn) { 11 | function makeDecorator (map) { 12 | return createDecorator((componentOptions, key) => { 13 | if (!componentOptions[bindTo]) { 14 | componentOptions[bindTo] = {} 15 | } 16 | 17 | let mapObject = { [key]: map } 18 | 19 | componentOptions[bindTo][key] = mapFn(mapObject)[key] 20 | }) 21 | } 22 | 23 | function helper (proto, key) { 24 | if (typeof key === 'string') { 25 | return makeDecorator(key)(proto, key) 26 | } 27 | 28 | return makeDecorator(proto) 29 | } 30 | 31 | return helper 32 | } 33 | -------------------------------------------------------------------------------- /helpers/index.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { StoreonDispatch } from 'storeon'; 3 | 4 | type Computed = () => any; 5 | type ActionMethod = (...args: any[]) => Promise; 6 | type InlineComputed = T extends (...args: any[]) => infer R ? () => R : never 7 | type InlineMethod any> = T extends (fn: any, ...args: infer Args) => infer R ? (...args: Args) => R : never 8 | type CustomVue = Vue & Record; 9 | 10 | interface Mapper { 11 | (map: Key[]): { [K in Key]: R }; 12 | >(map: Map): { [K in keyof Map]: R }; 13 | } 14 | 15 | interface MapperForState { 16 | any> = {}>( 17 | map: Map 18 | ): { [K in keyof Map]: InlineComputed }; 19 | } 20 | 21 | interface MapperForAction { 22 | , ...args: any[]) => any>>( 23 | map: Map 24 | ): { [K in keyof Map]: InlineMethod }; 25 | } 26 | 27 | export declare const mapState: Mapper & MapperForState; 28 | export declare const mapDispatch: Mapper & MapperForAction; 29 | -------------------------------------------------------------------------------- /helpers/index.js: -------------------------------------------------------------------------------- 1 | const mapState = states => { 2 | let res = {} 3 | if (process.env.NODE_ENV !== 'production' && !isValidMap(states)) { 4 | console.error('Mapper parameter must be either an Array or an Object') 5 | } 6 | normalizeMap(states).forEach(({ key, val }) => { 7 | res[key] = function () { 8 | let state = this.$storeon.state 9 | 10 | return typeof val === 'function' ? val.call(this, state) : state[val] 11 | } 12 | }) 13 | return res 14 | } 15 | 16 | const mapDispatch = events => { 17 | let res = {} 18 | if (process.env.NODE_ENV !== 'production' && !isValidMap(events)) { 19 | console.error('Mapper parameter must be either an Array or an Object') 20 | } 21 | normalizeMap(events).forEach(({ key, val }) => { 22 | res[key] = function (...args) { 23 | let dispatch = this.$storeon.dispatch 24 | 25 | if (typeof val === 'function') { 26 | return val.apply(this, [dispatch].concat(args)) 27 | } else { 28 | return dispatch.apply(this.$storeon, [val].concat(args)) 29 | } 30 | } 31 | }) 32 | return res 33 | } 34 | 35 | module.exports = { mapState, mapDispatch } 36 | 37 | function normalizeMap (map) { 38 | if (!isValidMap(map)) { 39 | return [] 40 | } 41 | 42 | if (Array.isArray(map)) { 43 | return map.map(key => ({ key, val: key })) 44 | } else { 45 | return Object.keys(map).map(key => ({ key, val: map[key] })) 46 | } 47 | } 48 | 49 | function isObject (obj) { 50 | return obj !== null && typeof obj === 'object' 51 | } 52 | 53 | function isValidMap (map) { 54 | return Array.isArray(map) || isObject(map) 55 | } 56 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import { StoreonStore } from 'storeon' 3 | 4 | export declare type StoreonVueStore = 5 | StoreonStore & { state: State } 6 | 7 | 8 | export declare function createStoreonPlugin(store: StoreonStore): { 9 | install(app: App): void 10 | } 11 | 12 | export declare function useStoreon(): 13 | StoreonVueStore; 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { reactive, inject } = require('vue') 2 | 3 | const STORE_KEY = 'storeon' 4 | 5 | function createStoreonPlugin (store) { 6 | return { 7 | install (app) { 8 | if (process.env.NODE_ENV !== 'production' && !store) { 9 | throw new Error( 10 | 'Please provide store to the "createStoreonPlugin" function' 11 | ) 12 | } 13 | 14 | store.state = reactive(store.get()) 15 | 16 | app.provide(STORE_KEY, store) 17 | app.config.globalProperties.$storeon = store 18 | 19 | store.on('@changed', newState => { 20 | Object.assign(store.state, newState) 21 | }) 22 | } 23 | } 24 | } 25 | 26 | function useStoreon () { 27 | let store = inject(STORE_KEY) 28 | 29 | if (process.env.NODE_ENV !== 'production' && !store) { 30 | throw new Error( 31 | 'Could not find storeon context value. ' + 32 | 'Make sure you provide store using "createStoreonPlugin" function' 33 | ) 34 | } 35 | 36 | return store 37 | } 38 | 39 | module.exports = { createStoreonPlugin, useStoreon } 40 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storeon/vue", 3 | "version": "3.0.0-beta.2", 4 | "description": "A tiny (160 bytes) connector for Storeon and Vue", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "start": "parcel serve example/index.html --open", 9 | "lint": "eslint ./*.js", 10 | "test": "yarn lint && jest --coverage && size-limit" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/storeon/vue.git" 15 | }, 16 | "author": "Dmytro Mostovyi ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/storeon/vue/issues" 20 | }, 21 | "homepage": "https://github.com/storeon/vue#readme", 22 | "keywords": [ 23 | "vue", 24 | "storeon", 25 | "state" 26 | ], 27 | "files": [ 28 | "index.d.ts", 29 | "helpers/", 30 | "class/" 31 | ], 32 | "sideEffects": false, 33 | "peerDependencies": { 34 | "storeon": "^3.0.0", 35 | "vue": "^3.0.0", 36 | "vue-class-component": "^8.0.0-beta.4" 37 | }, 38 | "peerDependenciesMeta": { 39 | "vue-class-component": { 40 | "optional": true 41 | } 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.12.3", 45 | "@babel/plugin-proposal-class-properties": "^7.12.1", 46 | "@babel/plugin-proposal-decorators": "^7.12.1", 47 | "@logux/eslint-config": "^41.0.2", 48 | "@size-limit/dual-publish": "^4.7.0", 49 | "@size-limit/preset-small-lib": "^4.7.0", 50 | "babel-eslint": "^10.1.0", 51 | "babel-jest": "^26.6.1", 52 | "dual-publish": "^0.11.0", 53 | "eslint": "^7.12.1", 54 | "eslint-config-standard": "^15.0.1", 55 | "eslint-plugin-import": "^2.22.1", 56 | "eslint-plugin-jest": "^24.1.0", 57 | "eslint-plugin-node": "^11.1.0", 58 | "eslint-plugin-prefer-let": "^1.1.0", 59 | "eslint-plugin-prettierx": "^0.14.0", 60 | "eslint-plugin-promise": "^4.2.1", 61 | "eslint-plugin-security": "^1.4.0", 62 | "eslint-plugin-standard": "^4.0.2", 63 | "eslint-plugin-unicorn": "^23.0.0", 64 | "husky": "^4.3.0", 65 | "jest": "^26.6.1", 66 | "lint-staged": "^10.5.0", 67 | "size-limit": "^4.7.0", 68 | "storeon": "^3.1.1", 69 | "vue": "^3.0.2", 70 | "vue-class-component": "^8.0.0-beta.4" 71 | }, 72 | "lint-staged": { 73 | "*.js": [ 74 | "eslint --fix", 75 | "git add" 76 | ] 77 | }, 78 | "husky": { 79 | "hooks": { 80 | "pre-commit": "lint-staged" 81 | } 82 | }, 83 | "browserslist": [ 84 | "last 1 chrome versions" 85 | ], 86 | "size-limit": [ 87 | { 88 | "name": "core", 89 | "import": { 90 | "index.js": "{ provideStoreon, useStoreon }" 91 | }, 92 | "ignore": [ 93 | "vue" 94 | ], 95 | "limit": "160 B" 96 | }, 97 | { 98 | "name": "core + helpers", 99 | "import": { 100 | "index.js": "{ StoreonVue }", 101 | "helpers/index.js": "{ mapState, mapDispatch }" 102 | }, 103 | "ignore": [ 104 | "vue" 105 | ], 106 | "limit": "342 B" 107 | }, 108 | { 109 | "name": "core + class", 110 | "import": { 111 | "index.js": "{ StoreonVue }", 112 | "class/index.js": "{ State, Dispatch }" 113 | }, 114 | "ignore": [ 115 | "vue" 116 | ], 117 | "limit": "447 B" 118 | } 119 | ], 120 | "eslintConfig": { 121 | "parser": "babel-eslint", 122 | "extends": "@logux/eslint-config" 123 | }, 124 | "eslintIgnore": [ 125 | "example" 126 | ], 127 | "jest": { 128 | "transform": { 129 | "^.+\\.js$": "babel-jest" 130 | }, 131 | "coveragePathIgnorePatterns": [ 132 | "test/utils.js" 133 | ] 134 | }, 135 | "babel": { 136 | "plugins": [ 137 | [ 138 | "@babel/plugin-proposal-decorators", 139 | { 140 | "legacy": true 141 | } 142 | ], 143 | [ 144 | "@babel/plugin-proposal-class-properties", 145 | { 146 | "loose": false 147 | } 148 | ] 149 | ] 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /test/unit/class.test.js: -------------------------------------------------------------------------------- 1 | const { Vue } = require('vue-class-component') 2 | 3 | const { createStore, mount } = require('../utils') 4 | const { createStoreonPlugin } = require('../../index') 5 | const { State, Dispatch } = require('../../class') 6 | 7 | it('state (type)', () => { 8 | let store = createStore() 9 | 10 | class MyComp extends Vue { 11 | @State('count') foo 12 | } 13 | 14 | let vm = mount(createStoreonPlugin(store), MyComp) 15 | 16 | expect(vm.foo).toBe(0) 17 | }) 18 | 19 | it('state (function)', () => { 20 | let store = createStore() 21 | 22 | class MyComp extends Vue { 23 | @State(({ count }) => count + 10) 24 | foo 25 | } 26 | 27 | let vm = mount(createStoreonPlugin(store), MyComp) 28 | 29 | expect(vm.foo).toBe(10) 30 | }) 31 | 32 | it('state (implicit state name)', () => { 33 | let store = createStore() 34 | 35 | class MyComp extends Vue { 36 | @State count 37 | } 38 | 39 | let vm = mount(createStoreonPlugin(store), MyComp) 40 | 41 | expect(vm.count).toBe(0) 42 | }) 43 | 44 | it('dispatch (type)', () => { 45 | let store = createStore() 46 | jest.spyOn(store, 'dispatch') 47 | 48 | class MyComp extends Vue { 49 | @Dispatch('foo/set') 50 | bar 51 | } 52 | 53 | let vm = mount(createStoreonPlugin(store), MyComp) 54 | 55 | vm.bar('bar') 56 | expect(store.dispatch.mock.calls[0][1]).toBe('bar') 57 | }) 58 | 59 | it('dispatch (implicity action type)', () => { 60 | let store = createStore() 61 | jest.spyOn(store, 'dispatch') 62 | 63 | class MyComp extends Vue { 64 | @Dispatch inc 65 | } 66 | 67 | let vm = mount(createStoreonPlugin(store), MyComp) 68 | 69 | vm.inc() 70 | expect(store.dispatch.mock.calls[0][0]).toBe('inc') 71 | }) 72 | -------------------------------------------------------------------------------- /test/unit/helpers.test.js: -------------------------------------------------------------------------------- 1 | const { createStore, mount } = require('../utils') 2 | const { mapState, mapDispatch } = require('../../helpers') 3 | const { createStoreonPlugin } = require('../../index') 4 | 5 | let warn 6 | 7 | beforeEach(() => { 8 | warn = jest.spyOn(global.console, 'error').mockImplementation(() => null) 9 | }) 10 | 11 | afterEach(() => { 12 | warn.mockRestore() 13 | }) 14 | 15 | it('mapState (array)', () => { 16 | let store = createStore() 17 | 18 | let vm = mount(createStoreonPlugin(store), { 19 | computed: mapState(['count']) 20 | }) 21 | expect(vm.count).toBe(0) 22 | store.dispatch('inc') 23 | expect(vm.count).toBe(1) 24 | }) 25 | 26 | it('mapState (object)', () => { 27 | let store = createStore() 28 | let vm = mount(createStoreonPlugin(store), { 29 | computed: mapState({ 30 | a: state => { 31 | return state.count + 1 32 | } 33 | }) 34 | }) 35 | expect(vm.a).toBe(1) 36 | store.dispatch('inc') 37 | expect(vm.a).toBe(2) 38 | }) 39 | 40 | it('mapState (with undefined states)', () => { 41 | jest.spyOn(console, 'error') 42 | let store = createStore() 43 | let vm = mount(createStoreonPlugin(store), { 44 | computed: mapState('foo') 45 | }) 46 | expect(vm.count).toBeUndefined() 47 | expect(console.error).toHaveBeenCalledWith( 48 | 'Mapper parameter must be either an Array or an Object' 49 | ) 50 | }) 51 | 52 | it('mapDispatch (array)', () => { 53 | let store = createStore() 54 | jest.spyOn(store, 'dispatch') 55 | let vm = mount(createStoreonPlugin(store), { 56 | methods: mapDispatch(['inc', 'foo/set']) 57 | }) 58 | vm.inc() 59 | expect(store.dispatch).toHaveBeenCalledWith('inc') 60 | expect(store.dispatch).not.toHaveBeenCalledWith('foo/set') 61 | vm['foo/set']() 62 | expect(store.dispatch).toHaveBeenCalledWith('foo/set') 63 | }) 64 | 65 | it('mapDispatch (object)', () => { 66 | let store = createStore() 67 | jest.spyOn(store, 'dispatch') 68 | let vm = mount(createStoreonPlugin(store), { 69 | methods: mapDispatch({ 70 | foo: 'inc', 71 | bar: 'foo/set' 72 | }) 73 | }) 74 | vm.bar('bar') 75 | expect(store.dispatch).toHaveBeenCalledWith('foo/set', 'bar') 76 | expect(store.dispatch).not.toHaveBeenCalledWith('inc') 77 | expect(vm.$storeon.state.foo).toBe('bar') 78 | vm.foo() 79 | expect(store.dispatch).toHaveBeenCalledWith('inc') 80 | }) 81 | 82 | it('mapDispatch (function)', () => { 83 | let store = createStore() 84 | jest.spyOn(store, 'dispatch') 85 | let vm = mount(createStoreonPlugin(store), { 86 | methods: mapDispatch({ 87 | foo (dispatch, arg) { 88 | dispatch('a', arg + 'bar') 89 | } 90 | }) 91 | }) 92 | vm.foo('foo') 93 | expect(store.dispatch.mock.calls[0][1]).toBe('foobar') 94 | }) 95 | 96 | it('mapDispatch (with undefined actions)', () => { 97 | jest.spyOn(console, 'error') 98 | let store = createStore() 99 | jest.spyOn(store, 'dispatch') 100 | let vm = mount(createStoreonPlugin(store), { 101 | store, 102 | methods: mapDispatch('inc') 103 | }) 104 | expect(vm.count).toBeUndefined() 105 | expect(store.dispatch).not.toHaveBeenCalled() 106 | expect(console.error).toHaveBeenCalledWith( 107 | 'Mapper parameter must be either an Array or an Object' 108 | ) 109 | }) 110 | -------------------------------------------------------------------------------- /test/unit/index.test.js: -------------------------------------------------------------------------------- 1 | const { nextTick } = require('vue') 2 | 3 | const { mount, createStore } = require('../utils') 4 | const { createStoreonPlugin, useStoreon } = require('../../index') 5 | 6 | it('should inject store to the global properties', () => { 7 | let store = createStore() 8 | 9 | let vm = mount(createStoreonPlugin(store)) 10 | 11 | expect(vm.$storeon.state).toEqual(store.get()) 12 | }) 13 | 14 | it('should provide store', async () => { 15 | let store = createStore() 16 | let vm = mount(createStoreonPlugin(store), { 17 | setup () { 18 | return useStoreon() 19 | } 20 | }) 21 | 22 | expect(vm.state.count).toBe(0) 23 | 24 | vm.dispatch('inc') 25 | await nextTick() 26 | 27 | expect(vm.state.count).toBe(1) 28 | }) 29 | 30 | it('should throw an error when no store passed to the function', () => { 31 | expect(() => mount(createStoreonPlugin())).toThrow( 32 | new Error('Please provide store to the "createStoreonPlugin" function') 33 | ) 34 | }) 35 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const { createApp } = require('vue') 2 | const { createStoreon } = require('storeon') 3 | 4 | function mount (store, component = {}) { 5 | let el = createElement() 6 | 7 | component.render = () => {} 8 | 9 | let app = createApp(component) 10 | 11 | app.use(store) 12 | 13 | return app.mount(el) 14 | } 15 | 16 | function createStore () { 17 | let counter = storeon => { 18 | storeon.on('@init', () => ({ count: 0, foo: 'baz' })) 19 | storeon.on('inc', ({ count }) => ({ count: count + 1 })) 20 | storeon.on('foo/set', (_, data) => ({ foo: data })) 21 | } 22 | 23 | return createStoreon([counter]) 24 | } 25 | 26 | module.exports = { mount, createStore } 27 | 28 | function createElement () { 29 | let el = document.createElement('div') 30 | 31 | document.body.appendChild(el) 32 | 33 | return el 34 | } 35 | --------------------------------------------------------------------------------