├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── example
├── index.html
├── src
│ ├── api
│ │ └── shop.ts
│ ├── app.ts
│ ├── components
│ │ ├── App.vue
│ │ ├── ProductList.vue
│ │ └── ShoppingCart.vue
│ ├── shims-vue.d.ts
│ └── store
│ │ ├── cart.ts
│ │ ├── index.ts
│ │ └── products.ts
├── tsconfig.json
└── webpack.config.js
├── jestconfig.json
├── package-lock.json
├── package.json
├── src
├── VuexModule.ts
├── actions.ts
├── index.ts
├── module-factory.ts
├── module.ts
└── mutations.ts
├── test
├── actions-inheritance.ts
├── actions.ts
├── constructor.ts
├── generate-mutations.ts
├── getters-inheritance.ts
├── getters.ts
├── instanceof.ts
├── local-functions-inheritance.ts
├── local-functions.ts
├── module-reference.ts
├── mutations-inheritance.ts
├── mutations.ts
├── state.ts
├── tsconfig.json
└── watch.ts
├── tsconfig.json
└── tslint.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /lib
3 | /commonjs
4 | /example/build.js
5 | /example/build.js.map
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.rulers": [ 120 ],
3 | "prettier.printWidth": 120,
4 | "prettier.disableLanguages": [],
5 |
6 | "[typescript]": {
7 | "editor.formatOnSave": true
8 | },
9 | "[vue]": {
10 | "editor.formatOnSave": true
11 | },
12 | "tslint.autoFixOnSave": true,
13 |
14 | "vetur.format.defaultFormatter.html": "none",
15 | "vetur.format.defaultFormatter.ts": "none"
16 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 gertqin
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 | # vuex-class-modules
2 |
3 | This is yet another package to introduce a simple type-safe class style syntax for your vuex modules, inspired by [vue-class-component](https://github.com/vuejs/vue-class-component).
4 |
5 | [](https://www.npmjs.com/package/vuex-class-modules)
6 |
7 | ## Installation
8 |
9 | `npm install vuex-class-modules`
10 |
11 | And make sure to have the `--experimentalDecorators` flag enabled.
12 |
13 | Both a `commonjs` and a `esm` module build are published. If you have a webpack-based setup, it will use the `esm` modules by default.
14 |
15 | ## Usage
16 |
17 | Vuex modules can be written using decorators as a class:
18 |
19 | ```typescript
20 | // user-module.ts
21 | import { VuexModule, Module, Mutation, Action } from "vuex-class-modules";
22 |
23 | @Module
24 | class UserModule extends VuexModule {
25 | // state
26 | firstName = "Foo";
27 | lastName = "Bar";
28 |
29 | // getters
30 | get fullName() {
31 | return this.firstName + " " + this.lastName;
32 | }
33 |
34 | // mutations
35 | @Mutation
36 | setFirstName(firstName: string) {
37 | this.firstName = firstName;
38 | }
39 | @Mutation
40 | setLastName(lastName: string) {
41 | this.lastName = lastName;
42 | }
43 |
44 | // actions
45 | @Action
46 | async loadUser() {
47 | const user = await fetchUser();
48 | this.setFirstName(user.firstName);
49 | this.setLastName(user.lastName);
50 | }
51 | }
52 |
53 | // register module (could be in any file)
54 | import store from "path/to/store";
55 | export const userModule = new UserModule({ store, name: "user" });
56 | ```
57 |
58 | The module will automatically be registered to the store as a namespaced dynamic module when it is instantiated. (The modules are namespaced to avoid name conflicts between modules for getters/mutations/actions.)
59 |
60 | The module can then be used in vue components as follows:
61 |
62 | ```ts
63 | // MyComponent.vue
64 | import Vue from "vue";
65 | import { userModule } from "path/to/user-module.ts";
66 |
67 | export class MyComponent extends Vue {
68 | get firstName() {
69 | return userModule.firstName; // -> store.state.user.firstName
70 | }
71 | get fullName() {
72 | return userModule.fullName; // -> store.getters["user/fullName]
73 | }
74 |
75 | created() {
76 | userModule.setFirstName("Foo"); // -> store.commit("user/setFirstName", "Foo")
77 | userModule.loadUser(); // -> store.dispatch("user/loadUser")
78 | }
79 | }
80 | ```
81 |
82 | ### What about `rootState` and `rootGetters`?
83 |
84 | There are two ways to access other modules within a module, or dispatch actions to other modules.
85 |
86 | 1. Simply import the instantiated module (suitable if the modules are instantiated in the same file as they are defined):
87 |
88 | ```ts
89 | // my-module.ts
90 |
91 | // import the module instance
92 | import { otherModule } from "./other-module";
93 |
94 | @Module
95 | class MyModule extends VuexModule {
96 | get myGetter() {
97 | return otherModule.foo;
98 | }
99 |
100 | @Action
101 | async myAction() {
102 | await otherModule.someAction();
103 | // ...
104 | }
105 | }
106 | ```
107 |
108 | 2. The other module can be registered through the constructor (suitable if the modules are instantiated elsewhere)
109 |
110 | ```ts
111 | // my-module.ts
112 |
113 | // import the class, not the instance
114 | import { OtherModule } from "./other-module";
115 |
116 | @Module
117 | export class MyModule extends VuexModule {
118 | private otherModule: OtherModule;
119 |
120 | constructor(otherModule: OtherModule, options: RegisterOptions) {
121 | super(options);
122 | this.otherModule = otherModule;
123 | }
124 |
125 | get myGetter() {
126 | return this.otherModule.foo;
127 | }
128 |
129 | @Action
130 | async myAction() {
131 | await this.otherModule.someAction();
132 | // ...
133 | }
134 | }
135 |
136 | // register-modules.ts
137 | import store from "path/to/store";
138 | import { OtherModule } from "path/to/other-module";
139 | import { MyModule } from "path/to/my-module";
140 |
141 | export const otherModule = new OtherModule({ store, name: "otherModule" });
142 | export const myModule = new MyModule(otherModule, { store, name: "myModule" });
143 | ```
144 |
145 | The local modules will not be part of the state and cannot be accessed from the outside, so they should always be declared private.
146 |
147 | ```ts
148 | myModule.otherModule; // -> undefined
149 | ```
150 |
151 | ### The `store.watch` function
152 |
153 | Vuex can also be used ouside of vue modules. To listen for changes to the state, vuex provides a [watch method](https://vuex.vuejs.org/api/#watch).
154 |
155 | This api is also provided by vuex-class-modules under the method name `$watch` to prevent name collisions. For example you can do:
156 |
157 | ```ts
158 | import store from "./store";
159 | import { MyModule } from "./my-module";
160 |
161 | const myModule = new MyModule({ store, name: "MyModule" });
162 | myModule.$watch(
163 | (theModule) => theModule.fullName,
164 | (newName: string, oldName: string) => {
165 | // ...
166 | },
167 | {
168 | deep: false,
169 | immediate: false,
170 | }
171 | );
172 | ```
173 |
174 | and to unwatch:
175 |
176 | ```ts
177 | const unwatch = myModule.$watch(...);
178 | unwatch();
179 | ```
180 |
181 | ### Register options
182 |
183 | - `name` [required]: Name of the module
184 | - `store` [required]: The vuex store - which can just be instantiated as empty:
185 |
186 | ```ts
187 | // store.ts
188 | import Vue from "vue";
189 | import Vuex from "vuex";
190 | Vue.use(Vuex);
191 | const store = new Vuex.Store({});
192 | ```
193 |
194 | ### Module options
195 |
196 | The module decorator can also accept options:
197 |
198 | - `generateMutationSetters` [optional, default=false]: Whether automatic mutation setters for the state properties should be generated, see [Generate Mutation Setters](#generate-mutation-setters).
199 |
200 | ## Example
201 |
202 | The vuex shopping cart example rewritten using `vue-class-component` and `vuex-class-modules` can be found in the [example directory](/example). Build the example using:
203 |
204 | `npm run example`
205 |
206 | ## Caveats of `this`
207 |
208 | As for vue-class-component `this` inside the module is just a proxy object to the store. It can therefore only access what the corresponding vuex module function would be able to access:
209 |
210 | ```ts
211 | @Module
212 | class MyModule extends VuexModule {
213 | foo = "bar";
214 |
215 | get someGetter() {
216 | return 123;
217 | }
218 | get myGetter() {
219 | this.foo; // -> "bar"
220 | this.someGetter; // -> 123
221 | this.someMutation(); // undefined, getters cannot call mutations
222 | this.someAction(); // -> undefined, getters cannot call actions
223 | }
224 |
225 | @Mutation
226 | someMutation() {
227 | /* ... */
228 | }
229 | @Mutation
230 | myMutation() {
231 | this.foo; // -> "bar"
232 | this.someGetter; // -> undefined, mutations dont have access to getters
233 | this.someMutation(); // -> undefined, mutations cannot call other mutations
234 | this.someAction(); // -> undefined, mutations cannot call actions
235 | }
236 |
237 | @Action
238 | async someAction() {
239 | /* ... */
240 | }
241 | @Action
242 | async myAction() {
243 | this.foo; // -> "bar"
244 | this.someGetter; // -> 123
245 | this.myMutation(); // Ok
246 | await this.someAction(); // Ok
247 | }
248 | }
249 | ```
250 |
251 | ## Local Functions
252 |
253 | The module can have non-mutation/action functions which can be used inside the module. As for local modules, these functions will not be exposed outside the module and should therefore be private. `this` will be passed on to the local function from the getter/mutation/action.
254 |
255 | ```ts
256 | @Module
257 | class MyModule extends VuexModule {
258 | get myGetter() {
259 | return myGetterHelper();
260 | }
261 | private myGetterHelper() {
262 | // same 'this' context as myGetter
263 | }
264 |
265 | @Mutation
266 | myMutation() {
267 | this.myMutationHelper();
268 | }
269 |
270 | // should be private
271 | myMutationHelper() { /* ... */}
272 | }
273 | const myModule = new MyModule({ store, name: "myModule });
274 | myModule.myMutationHelper // -> undefined.
275 | ```
276 |
277 | ## Generate Mutation Setters
278 |
279 | As I often find myself writing a lot of simple setter mutations like
280 |
281 | ```ts
282 | @Module
283 | class UserModule extends VuexModule {
284 | firstName = "Foo";
285 | lastName = "Bar";
286 |
287 | @Mutation
288 | setFirstName(firstName: string) {
289 | this.firstName = firstName;
290 | }
291 | @Mutation
292 | setLastName(lastName: string) {
293 | this.lastName = lastName;
294 | }
295 | }
296 | ```
297 |
298 | a module option `generateMutationSetters` has been added, which when enabled will generate a setter mutation for each state property. The state can then be modified directly from the actions:
299 |
300 | ```ts
301 | @Module({ generateMutationSetters: true })
302 | class UserModule extends VuexModule {
303 | firstName = "Foo";
304 | lastName = "Bar";
305 |
306 | // Auto generated:
307 | // @Mutation set__firstName(val: any) { this.firstName = val }
308 | // @Mutation set__lastName(val: any) { this.lastName = val }
309 |
310 | @Action
311 | async loadUser() {
312 | const user = await fetchUser();
313 | this.firstName = user.firstName; // -> this.set__firstName(user.firstName);
314 | this.lastName = user.lastName; // -> this.set__lastName(user.lastName);
315 | }
316 | }
317 | ```
318 |
319 | _NOTE:_ Setters are only generated for root-level state properties, so in order to update a property of an object you have to use a mutation or replace the entire object:
320 |
321 | ```ts
322 | @Module({ generateMutationSetters: true })
323 | class UserModule extends VuexModule {
324 | user = {
325 | id: 123,
326 | name: "Foo",
327 | };
328 |
329 | @Mutation
330 | setUserName() {
331 | this.user.name = "Bar"; // OK!
332 | }
333 |
334 | @Action
335 | async loadUser() {
336 | this.user.name = "Bar"; // Bad, the state is mutated outside a mutation
337 | this.user = { ...this.user, name: "Bar" }; // OK!
338 | }
339 | }
340 | ```
341 |
342 | ## Vite HMR
343 |
344 | [Vite](https://vitejs.dev/) (and possibly other bundlers) uses `import.meta.hot` for HMR, which `vuex-class-modules` doesn't support currently. Instead a static property
345 |
346 | ```ts
347 | VuexModule.__useHotUpdate = true; // default false
348 | ```
349 |
350 | is provided, which will force hot updates to the store instead of throwing an error when a module with a duplicate name is registered. This could for instance be set only in dev mode
351 |
352 | ```ts
353 | VuexModule.__useHotUpdate = import.meta.env.DEV;
354 | ```
355 |
356 | ## License
357 |
358 | [MIT](http://opensource.org/licenses/MIT)
359 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vuex Class Modules Example
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/src/api/shop.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Mocking client-server processing
3 | */
4 | export interface Product {
5 | id: number;
6 | title: string;
7 | price: number;
8 | inventory: number;
9 | }
10 |
11 | export interface CartItem {
12 | id: number;
13 | quantity: number;
14 | }
15 |
16 | const products: Product[] = [
17 | { id: 1, title: "iPad 4 Mini", price: 500.01, inventory: 2 },
18 | { id: 2, title: "H&M T-Shirt White", price: 10.99, inventory: 10 },
19 | { id: 3, title: "Charli XCX - Sucker CD", price: 19.99, inventory: 5 }
20 | ];
21 |
22 | export default {
23 | async getProducts() {
24 | await new Promise(resolve => setTimeout(resolve, 100));
25 | return products;
26 | },
27 |
28 | async buyProducts(items: CartItem[]) {
29 | await new Promise(resolve => setTimeout(resolve, 100));
30 |
31 | if (Math.random() > 0.5) {
32 | throw Error();
33 | }
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/example/src/app.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import App from "./components/App.vue";
3 | import store from "./store";
4 |
5 | new Vue({
6 | el: "#app",
7 | store,
8 | render: h => h(App)
9 | });
10 |
--------------------------------------------------------------------------------
/example/src/components/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Shopping Cart Example
4 |
5 |
Products
6 |
7 |
8 |
9 |
10 |
11 |
12 |
23 |
--------------------------------------------------------------------------------
/example/src/components/ProductList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 | {{ product.title }} - {{ product.price }}€
5 |
6 |
7 |
8 |
9 |
10 |
32 |
--------------------------------------------------------------------------------
/example/src/components/ShoppingCart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Your Cart
4 |
Please add some products to cart.
5 |
6 | -
7 | {{ product.title }} - {{ product.price }}€ x {{ product.quantity }}
8 |
9 |
10 |
Total: {{ total }}€
11 |
12 |
Checkout {{ checkoutStatus }}.
13 |
14 |
15 |
16 |
38 |
--------------------------------------------------------------------------------
/example/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.vue" {
2 | import Vue from "vue";
3 | export default Vue;
4 | }
5 |
--------------------------------------------------------------------------------
/example/src/store/cart.ts:
--------------------------------------------------------------------------------
1 | import store from "./";
2 | import shop, { CartItem, Product } from "../api/shop";
3 | import { Module, Mutation, Action, VuexModule } from "../../../lib/index";
4 | import { productsModule } from "./products";
5 |
6 | @Module({ generateMutationSetters: true })
7 | class Cart extends VuexModule {
8 | items: CartItem[] = [];
9 | checkoutStatus = "";
10 |
11 | get cartProducts() {
12 | return this.items.map(({ id, quantity }) => {
13 | const product = productsModule.all.find(p => p.id === id);
14 | return {
15 | title: product!.title,
16 | price: product!.price,
17 | quantity
18 | };
19 | });
20 | }
21 |
22 | get cartTotalPrice() {
23 | return this.cartProducts.reduce((total, product) => {
24 | return total + product.price * product.quantity;
25 | }, 0);
26 | }
27 |
28 | @Mutation
29 | pushProductToCart(id: number) {
30 | this.items.push({
31 | id,
32 | quantity: 1
33 | });
34 | }
35 |
36 | @Mutation
37 | incrementItemQuantity(id: number) {
38 | const cartItem = this.items.find(item => item.id === id);
39 | cartItem!.quantity++;
40 | }
41 |
42 | @Action
43 | async addProductToCart(product: Product) {
44 | this.checkoutStatus = "";
45 |
46 | if (product.inventory > 0) {
47 | const cartItem = this.items.find(item => item.id === product.id);
48 | if (!cartItem) {
49 | this.pushProductToCart(product.id);
50 | } else {
51 | this.incrementItemQuantity(cartItem.id);
52 | }
53 | // remove 1 item from stock
54 | productsModule.decrementProductInventory(product.id);
55 | }
56 | }
57 |
58 | @Action
59 | async checkout() {
60 | const savedCartItems = [...this.items];
61 | this.checkoutStatus = "";
62 |
63 | // empty cart
64 | this.items = [];
65 |
66 | try {
67 | await shop.buyProducts(savedCartItems);
68 | this.checkoutStatus = "successful";
69 | } catch (e) {
70 | this.items = savedCartItems;
71 | this.checkoutStatus = "failed";
72 | }
73 | }
74 | }
75 |
76 | export const cartModule = new Cart({ store, name: "cart" });
77 |
--------------------------------------------------------------------------------
/example/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex from "vuex";
3 | Vue.use(Vuex);
4 |
5 | export default new Vuex.Store({});
6 |
--------------------------------------------------------------------------------
/example/src/store/products.ts:
--------------------------------------------------------------------------------
1 | import store from "./";
2 | import shop, { Product } from "../api/shop";
3 | import { Module, Mutation, Action, VuexModule } from "../../../lib/index";
4 |
5 | @Module({ generateMutationSetters: true })
6 | class Products extends VuexModule {
7 | all: Product[] = [];
8 |
9 | @Mutation
10 | decrementProductInventory(id: number) {
11 | const product = this.all.find(p => p.id === id);
12 | product!.inventory--;
13 | }
14 |
15 | @Action
16 | async getAllProducts() {
17 | this.all = await shop.getProducts();
18 | }
19 | }
20 |
21 | export const productsModule = new Products({ store, name: "products" });
22 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "lib": [
5 | "dom",
6 | "esnext"
7 | ],
8 | "module": "es2015",
9 | "moduleResolution": "node",
10 | "experimentalDecorators": true,
11 | "strict": true,
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": false
14 | },
15 | "include": [
16 | "./**/*.ts",
17 | ]
18 | }
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const VueLoaderPlugin = require('vue-loader/lib/plugin')
2 |
3 | module.exports = {
4 | mode: 'development',
5 | context: __dirname,
6 | entry: './src/app.ts',
7 | output: {
8 | path: __dirname,
9 | filename: 'build.js'
10 | },
11 | resolve: {
12 | extensions: ['.ts', '.tsx', '.js']
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /\.tsx?$/,
18 | exclude: /node_modules/,
19 | use: [
20 | {
21 | loader: 'ts-loader',
22 | options: {
23 | appendTsSuffixTo: [/\.vue$/],
24 | appendTsxSuffixTo: [/\.vue$/]
25 | }
26 | }
27 | ]
28 | },
29 | {
30 | test: /\.vue$/,
31 | use: ['vue-loader']
32 | }
33 | ]
34 | },
35 | devtool: 'source-map',
36 | plugins: [
37 | new VueLoaderPlugin()
38 | ]
39 | }
--------------------------------------------------------------------------------
/jestconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "ts-jest",
3 | "rootDir": "./test",
4 | "testMatch": ["/**/*.ts"],
5 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
6 | "globals": {
7 | "ts-jest": {
8 | "tsConfig": {
9 | "isolatedModules": false,
10 | "esModuleInterop": true
11 | }
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vuex-class-modules",
3 | "version": "1.3.0",
4 | "description": "Typescript class decorators for class-style vuex modules.",
5 | "main": "commonjs/index.js",
6 | "module": "lib/index.js",
7 | "exports": {
8 | ".": {
9 | "require": "./commonjs/index.js",
10 | "default": "./lib/index.js"
11 | }
12 | },
13 | "types": "lib/index.d.ts",
14 | "files": [
15 | "lib",
16 | "commonjs"
17 | ],
18 | "scripts": {
19 | "test": "jest --config jestconfig.json",
20 | "build": "npm run build:es2015 && npm run build:commonjs",
21 | "build:es2015": "tsc",
22 | "build:commonjs": "tsc -m commonjs --outDir ./commonjs",
23 | "lint": "tslint -p tsconfig.json --fix",
24 | "example": "npm run build && webpack --config ./example/webpack.config.js",
25 | "prepare": "npm run build",
26 | "prepublishOnly": "npm test && npm run lint",
27 | "preversion": "npm run lint",
28 | "version": "git add -A src",
29 | "postversion": "git push && git push --tags"
30 | },
31 | "repository": {
32 | "type": "git",
33 | "url": "git+https://github.com/gertqin/vuex-class-modules.git"
34 | },
35 | "keywords": [
36 | "vue",
37 | "vuex",
38 | "typescript",
39 | "class",
40 | "decorators"
41 | ],
42 | "author": "Gert Qin Hansen",
43 | "license": "MIT",
44 | "bugs": {
45 | "url": "https://github.com/gertqin/vuex-class-modules/issues"
46 | },
47 | "homepage": "https://github.com/gertqin/vuex-class-modules#readme",
48 | "devDependencies": {
49 | "@types/node": "12.12.2",
50 | "@types/jest": "^26.0.14",
51 | "@types/webpack-env": "^1.13.9",
52 | "css-loader": "^3.2.0",
53 | "jest": "^26.4.0",
54 | "ts-jest": "^26.4.0",
55 | "ts-loader": "^6.2.0",
56 | "tslint": "^5.20.0",
57 | "tslint-config-prettier": "^1.18.0",
58 | "tslint-plugin-prettier": "^2.0.1",
59 | "typescript": "^3.8.4",
60 | "vue": "^2.6.10",
61 | "vue-class-component": "^7.1.0",
62 | "vue-loader": "^15.7.0",
63 | "vue-template-compiler": "^2.6.10",
64 | "vuex": "^3.1.1",
65 | "webpack": "^4.44.2",
66 | "webpack-cli": "^3.3.12"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/VuexModule.ts:
--------------------------------------------------------------------------------
1 | import { RegisterOptions } from "./module-factory";
2 | import { WatchOptions } from "vue";
3 |
4 | export class VuexModule {
5 | private __options: RegisterOptions;
6 |
7 | static __useHotUpdate: boolean = false;
8 |
9 | constructor(options: RegisterOptions) {
10 | this.__options = options;
11 | }
12 |
13 | $watch(fn: (arg: this) => T, callback: (newValue: T, oldValue: T) => void, options?: WatchOptions): Function {
14 | return function() {};
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/actions.ts:
--------------------------------------------------------------------------------
1 | import { ModulePrototype } from "./module-factory";
2 |
3 | export function Action(
4 | target: T,
5 | key: string | symbol,
6 | descriptor: TypedPropertyDescriptor<(arg?: any) => any>
7 | ) {
8 | const vuexModule = target.constructor as ModulePrototype;
9 | if (!vuexModule.__actions) {
10 | vuexModule.__actions = {};
11 | }
12 | if (descriptor.value) {
13 | vuexModule.__actions[key as string] = descriptor.value;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { RegisterOptions, ModuleOptions } from "./module-factory";
2 | export { Action } from "./actions";
3 | export { Module } from "./module";
4 | export { Mutation } from "./mutations";
5 | export { VuexModule } from "./VuexModule";
6 |
--------------------------------------------------------------------------------
/src/module-factory.ts:
--------------------------------------------------------------------------------
1 | import { Store, Module as StoreModule, GetterTree, ActionContext, Dispatch, Commit } from "vuex";
2 | import { WatchOptions } from "vue";
3 | import { VuexModule } from "./VuexModule";
4 |
5 | export interface ModuleOptions {
6 | generateMutationSetters?: boolean;
7 | }
8 |
9 | export interface RegisterOptions {
10 | store: Store;
11 | name: string;
12 | }
13 |
14 | export interface IVuexModule extends Dictionary {
15 | __options: RegisterOptions;
16 | }
17 | export interface IModulePrototype {
18 | __mutations?: Dictionary<(payload?: any) => void>;
19 | __actions?: Dictionary<(payload?: any) => Promise>;
20 | }
21 | export type ModulePrototype = IModulePrototype & Function;
22 |
23 | type Dictionary = { [k: string]: T };
24 |
25 | interface ModuleDefinition {
26 | state: Dictionary;
27 | moduleRefs: Dictionary;
28 | getters: Dictionary<() => void>;
29 | mutations: Dictionary<(payload?: any) => void>;
30 | actions: Dictionary<(payload?: any) => Promise>;
31 | localFunctions: Dictionary<(...args: any[]) => any>;
32 | }
33 | interface StoreProxyDefinition {
34 | state?: Dictionary;
35 | stateSetter?: (key: string, val: any) => void;
36 |
37 | getters?: Dictionary;
38 | commit?: Commit;
39 | dispatch?: Dispatch;
40 |
41 | useNamespaceKey?: boolean;
42 | excludeModuleRefs?: boolean;
43 | excludeLocalFunctions?: boolean;
44 | }
45 |
46 | export class VuexClassModuleFactory {
47 | moduleOptions: ModuleOptions;
48 | instance: IVuexModule;
49 | registerOptions: RegisterOptions;
50 |
51 | definition: ModuleDefinition = {
52 | state: {},
53 | moduleRefs: {},
54 | getters: {},
55 | mutations: {},
56 | actions: {},
57 | localFunctions: {}
58 | };
59 |
60 | constructor(classModule: ModulePrototype, instance: IVuexModule, moduleOptions: ModuleOptions) {
61 | this.moduleOptions = moduleOptions;
62 | this.instance = instance;
63 | this.registerOptions = instance.__options;
64 | this.init(classModule);
65 | }
66 |
67 | private init(classModule: ModulePrototype) {
68 | // state
69 | for (const key of Object.keys(this.instance)) {
70 | const val = this.instance[key];
71 | if (key !== "__options" && this.instance.hasOwnProperty(key)) {
72 | if (val instanceof VuexModule) {
73 | this.definition.moduleRefs[key] = val;
74 | } else {
75 | this.definition.state[key] = this.instance[key];
76 | }
77 | }
78 | }
79 |
80 | const actionKeys = Object.keys(classModule.__actions || {});
81 | const mutationKeys = Object.keys(classModule.__mutations || {});
82 | const isAction = (key: string) => actionKeys.indexOf(key) !== -1;
83 | const isMutation = (key: string) => mutationKeys.indexOf(key) !== -1;
84 |
85 | for (const module of getModulePrototypes(classModule)) {
86 | for (const key of Object.getOwnPropertyNames(module.prototype)) {
87 | const descriptor = Object.getOwnPropertyDescriptor(module.prototype, key) as PropertyDescriptor;
88 |
89 | const isGetter = !!descriptor.get;
90 | if (isGetter && !(key in this.definition.getters)) {
91 | this.definition.getters[key] = descriptor.get!;
92 | }
93 |
94 | if (isAction(key) && !(key in this.definition.actions) && descriptor.value) {
95 | this.definition.actions[key] = module.prototype[key];
96 | }
97 | if (isMutation(key) && !(key in this.definition.mutations) && descriptor.value) {
98 | this.definition.mutations[key] = module.prototype[key];
99 | }
100 |
101 | const isHelperFunction =
102 | descriptor.value &&
103 | typeof module.prototype[key] === "function" &&
104 | !isAction(key) &&
105 | !isMutation(key) &&
106 | key !== "constructor";
107 |
108 | if (isHelperFunction && !(key in this.definition.localFunctions)) {
109 | this.definition.localFunctions[key] = module.prototype[key];
110 | }
111 | }
112 | }
113 | }
114 |
115 | registerVuexModule() {
116 | const vuexModule: StoreModule = {
117 | state: this.definition.state,
118 | getters: {},
119 | mutations: {},
120 | actions: {},
121 | namespaced: true
122 | };
123 |
124 | // getters
125 | mapValues(vuexModule.getters!, this.definition.getters, getter => {
126 | return (state: any, getters: GetterTree) => {
127 | const thisObj = this.buildThisProxy({ state, getters });
128 | return getter.call(thisObj);
129 | };
130 | });
131 |
132 | // mutations
133 | mapValues(vuexModule.mutations!, this.definition.mutations, mutation => {
134 | return (state: any, payload: any) => {
135 | const thisObj = this.buildThisProxy({
136 | state,
137 | stateSetter: (stateField: string, val: any) => {
138 | state[stateField] = val;
139 | }
140 | });
141 | mutation.call(thisObj, payload);
142 | };
143 | });
144 | if (this.moduleOptions.generateMutationSetters) {
145 | for (const stateKey of Object.keys(this.definition.state)) {
146 | const mutation = (state: any, payload: any) => {
147 | state[stateKey] = payload;
148 | };
149 | vuexModule.mutations![this.getMutationSetterName(stateKey)] = mutation;
150 | }
151 | }
152 |
153 | // actions
154 | mapValues(vuexModule.actions!, this.definition.actions, action => {
155 | return (context: ActionContext, payload: any) => {
156 | const proxyDefinition: StoreProxyDefinition = {
157 | ...context,
158 | stateSetter: this.moduleOptions.generateMutationSetters
159 | ? (field: string, val: any) => {
160 | context.commit(this.getMutationSetterName(field), val);
161 | }
162 | : undefined
163 | };
164 | const thisObj = this.buildThisProxy(proxyDefinition);
165 |
166 | return action.call(thisObj, payload);
167 | };
168 | });
169 |
170 | // register module
171 | const { store, name } = this.registerOptions;
172 | if (store.state[name]) {
173 | if (VuexModule.__useHotUpdate || (typeof module !== "undefined" && module.hot)) {
174 | store.hotUpdate({
175 | modules: {
176 | [name]: vuexModule
177 | }
178 | });
179 | } else {
180 | throw Error(`[vuex-class-module]: A module with name '${name}' already exists.`);
181 | }
182 | } else {
183 | store.registerModule(this.registerOptions.name, vuexModule);
184 | }
185 | }
186 |
187 | buildAccessor() {
188 | const { store, name } = this.registerOptions;
189 |
190 | const stateSetter = this.moduleOptions.generateMutationSetters
191 | ? (field: string, val: any) => {
192 | store.commit(`${name}/${this.getMutationSetterName(field)}`, val);
193 | }
194 | : undefined;
195 |
196 | const accessorModule = this.buildThisProxy({
197 | ...store,
198 | state: store.state[name],
199 | stateSetter,
200 | useNamespaceKey: true,
201 | excludeModuleRefs: true,
202 | excludeLocalFunctions: true
203 | });
204 |
205 | // watch API
206 | accessorModule.$watch = (
207 | fn: (arg: VuexModule) => any,
208 | callback: (newValue: any, oldValue: any) => void,
209 | options?: WatchOptions
210 | ) => {
211 | return store.watch(
212 | (state: any, getters: any) =>
213 | fn(
214 | this.buildThisProxy({
215 | state: state[name],
216 | getters,
217 | useNamespaceKey: true
218 | })
219 | ),
220 | callback,
221 | options
222 | );
223 | };
224 |
225 | Object.setPrototypeOf(accessorModule, Object.getPrototypeOf(this.instance));
226 | Object.freeze(accessorModule);
227 |
228 | return accessorModule;
229 | }
230 |
231 | private buildThisProxy(proxyDefinition: StoreProxyDefinition) {
232 | const obj: any = {};
233 |
234 | if (proxyDefinition.state) {
235 | mapValuesToProperty(
236 | obj,
237 | this.definition.state,
238 | key => proxyDefinition.state![key],
239 | proxyDefinition.stateSetter
240 | ? (key, val) => proxyDefinition.stateSetter!(key, val)
241 | : () => {
242 | throw Error("[vuex-class-module]: Cannot modify state outside mutations.");
243 | }
244 | );
245 | }
246 | if (!proxyDefinition.excludeModuleRefs) {
247 | mapValues(obj, this.definition.moduleRefs, val => val);
248 | }
249 |
250 | const namespaceKey = proxyDefinition.useNamespaceKey ? this.registerOptions.name + "/" : "";
251 |
252 | if (proxyDefinition.getters) {
253 | mapValuesToProperty(obj, this.definition.getters, key => proxyDefinition.getters![`${namespaceKey}${key}`]);
254 | }
255 |
256 | if (proxyDefinition.commit) {
257 | mapValues(obj, this.definition.mutations, (mutation, key) => {
258 | return (payload?: any) => proxyDefinition.commit!(`${namespaceKey}${key}`, payload);
259 | });
260 | }
261 |
262 | if (proxyDefinition.dispatch) {
263 | mapValues(obj, this.definition.actions, (action, key) => {
264 | return (payload?: any) => proxyDefinition.dispatch!(`${namespaceKey}${key}`, payload);
265 | });
266 | }
267 |
268 | if (!proxyDefinition.excludeLocalFunctions) {
269 | mapValues(obj, this.definition.localFunctions, localFunction => {
270 | return (...args: any[]) => localFunction.apply(obj, args);
271 | });
272 | }
273 |
274 | return obj;
275 | }
276 |
277 | private getMutationSetterName(stateKey: string) {
278 | return "set__" + stateKey;
279 | }
280 | }
281 |
282 | function mapValues(target: Dictionary, source: Dictionary, mapFunc: (val: S, key: string) => V) {
283 | for (const key of Object.keys(source)) {
284 | target[key] = mapFunc(source[key], key);
285 | }
286 | }
287 |
288 | function mapValuesToProperty(
289 | target: Dictionary,
290 | source: Dictionary,
291 | get: (key: string) => any,
292 | set?: (key: string, val: any) => void
293 | ) {
294 | for (const key of Object.keys(source)) {
295 | Object.defineProperty(target, key, {
296 | get: () => get(key),
297 | set: set ? (val: string) => set(key, val) : undefined
298 | });
299 | }
300 | }
301 |
302 | function getModulePrototypes(module: ModulePrototype): ModulePrototype[] {
303 | const prototypes: ModulePrototype[] = [];
304 |
305 | for (let prototype = module; prototype && prototype !== VuexModule; prototype = Object.getPrototypeOf(prototype)) {
306 | prototypes.push(prototype);
307 | }
308 |
309 | return prototypes;
310 | }
311 |
--------------------------------------------------------------------------------
/src/module.ts:
--------------------------------------------------------------------------------
1 | import { VuexClassModuleFactory, ModuleOptions, IVuexModule } from "./module-factory";
2 | import { VuexModule } from "./VuexModule";
3 |
4 | type VuexModuleClass = new (...args: any[]) => VuexModule;
5 | export function Module(target: T): T;
6 | export function Module(options?: ModuleOptions): ClassDecorator;
7 | export function Module(arg?: ModuleOptions | T): ClassDecorator | T {
8 | if (typeof arg === "function") {
9 | return moduleDecoratorFactory()(arg) as T;
10 | } else {
11 | return moduleDecoratorFactory(arg);
12 | }
13 | }
14 |
15 | function moduleDecoratorFactory(moduleOptions?: ModuleOptions) {
16 | return (constructor: TFunction): TFunction => {
17 | const accessor: any = function(...args: any[]) {
18 | const instance = new constructor.prototype.constructor(...args) as IVuexModule;
19 | Object.setPrototypeOf(instance, accessor.prototype);
20 |
21 | const factory = new VuexClassModuleFactory(constructor, instance, moduleOptions || {});
22 |
23 | factory.registerVuexModule();
24 | return factory.buildAccessor();
25 | };
26 | accessor.prototype = Object.create(constructor.prototype);
27 | accessor.prototype.constructor = accessor;
28 | return accessor;
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/src/mutations.ts:
--------------------------------------------------------------------------------
1 | import { ModulePrototype } from "./module-factory";
2 |
3 | export function Mutation(
4 | target: T,
5 | key: string | symbol,
6 | descriptor: TypedPropertyDescriptor<(arg?: any) => void>
7 | ) {
8 | const vuexModule = target.constructor as ModulePrototype;
9 | if (!vuexModule.__mutations) {
10 | vuexModule.__mutations = {};
11 | }
12 | if (descriptor.value) {
13 | vuexModule.__mutations[key as string] = descriptor.value;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/test/actions-inheritance.ts:
--------------------------------------------------------------------------------
1 | import { Action, Module, Mutation, VuexModule, RegisterOptions } from "../src";
2 | import Vuex, { Store } from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 |
7 | abstract class ParentModule extends VuexModule {
8 | foo = "init";
9 |
10 | get bigFoo() {
11 | return this.foo.toUpperCase();
12 | }
13 |
14 | get decoratedFoo() {
15 | return `***${this.foo}***`;
16 | }
17 |
18 | @Mutation
19 | updateFoo(value: string) {
20 | this.foo = value;
21 | }
22 |
23 | @Action
24 | action1() {
25 | //
26 | }
27 |
28 | @Action
29 | action2() {
30 | //
31 | }
32 |
33 | @Action
34 | action3() {
35 | //
36 | }
37 |
38 | @Action
39 | action4() {
40 | //
41 | }
42 |
43 | @Action
44 | action5() {
45 | this.updateFoo(this.foo + "action5");
46 | }
47 |
48 | @Action
49 | action6() {
50 | this.updateFoo(this.foo + "polymorphicCallOf");
51 | this.action7();
52 | }
53 |
54 | abstract action7(): void;
55 | }
56 |
57 | @Module
58 | class Module1 extends ParentModule {
59 | private tag = "child1";
60 | foo: string = "init" + this.tag;
61 |
62 | get doubleFoo() {
63 | return this.foo + this.foo;
64 | }
65 |
66 | @Mutation
67 | setFooToExample() {
68 | this.foo = "example" + this.tag;
69 | }
70 |
71 | @Action
72 | action1() {
73 | this.setFooToExample();
74 | }
75 |
76 | @Action
77 | action2() {
78 | this.updateFoo("bar" + this.tag);
79 | }
80 |
81 | @Action
82 | action3() {
83 | this.updateFoo(this.doubleFoo);
84 | }
85 |
86 | @Action
87 | action4() {
88 | this.updateFoo(this.bigFoo);
89 | }
90 |
91 | @Action
92 | action5() {
93 | super.action5();
94 | this.updateFoo(this.foo + this.tag);
95 | }
96 |
97 | @Action
98 | action7(): void {
99 | this.updateFoo(this.foo + "action7" + this.tag);
100 | }
101 | }
102 |
103 | @Module
104 | class Module2 extends ParentModule {
105 | private tag = "child2";
106 |
107 | constructor(options: RegisterOptions) {
108 | super(options);
109 | this.foo = this.foo + this.tag;
110 | }
111 |
112 | get tripleFoo() {
113 | return this.foo + this.foo + this.foo;
114 | }
115 |
116 | @Mutation
117 | setFooToAnotherExample() {
118 | this.foo = "example" + this.tag;
119 | }
120 |
121 | @Action
122 | action1() {
123 | this.setFooToAnotherExample();
124 | }
125 |
126 | @Action
127 | action2() {
128 | this.updateFoo("baz" + this.tag);
129 | }
130 |
131 | @Action
132 | action3() {
133 | this.updateFoo(this.tripleFoo);
134 | }
135 |
136 | @Action
137 | action4() {
138 | this.updateFoo(this.decoratedFoo);
139 | }
140 |
141 | @Action
142 | action5() {
143 | super.action5();
144 | this.updateFoo(this.foo + this.tag);
145 | }
146 |
147 | @Action
148 | action7(): void {
149 | this.updateFoo(this.foo + "action7" + this.tag);
150 | }
151 | }
152 |
153 | describe("actions-inheritance", () => {
154 | let store: Store;
155 | let child1: Module1;
156 | let child2: Module2;
157 |
158 | beforeEach(() => {
159 | store = new Vuex.Store({});
160 | child1 = new Module1({ store, name: "child1" });
161 | child2 = new Module2({ store, name: "child2" });
162 | });
163 |
164 | test("overriden action has access to mutations", () => {
165 | child1.action1();
166 | child2.action1();
167 | expect(child1.foo).toBe("examplechild1");
168 | expect(child2.foo).toBe("examplechild2");
169 | });
170 |
171 | test("overriden action has access to parent mutations", () => {
172 | child1.action2();
173 | child2.action2();
174 | expect(child1.foo).toBe("barchild1");
175 | expect(child2.foo).toBe("bazchild2");
176 | });
177 |
178 | test("overriden action has access to getters", () => {
179 | child1.action3();
180 | child2.action3();
181 | expect(child1.foo).toBe("initchild1initchild1");
182 | expect(child2.foo).toBe("initchild2initchild2initchild2");
183 | });
184 |
185 | test("overriden action has access to parent getters", () => {
186 | child1.action4();
187 | child2.action4();
188 | expect(child1.foo).toBe("INITCHILD1");
189 | expect(child2.foo).toBe("***initchild2***");
190 | });
191 |
192 | test("overriden action has access to parent method implementation", () => {
193 | child1.action5();
194 | child2.action5();
195 | expect(child1.foo).toBe("initchild1action5child1");
196 | expect(child2.foo).toBe("initchild2action5child2");
197 | });
198 |
199 | test("parent action access derived action polymorphically", () => {
200 | child1.action6();
201 | child2.action6();
202 | expect(child1.foo).toBe("initchild1polymorphicCallOfaction7child1");
203 | expect(child2.foo).toBe("initchild2polymorphicCallOfaction7child2");
204 | });
205 | test("access action7 directly", () => {
206 | child1.action7();
207 | child2.action7();
208 | expect(child1.foo).toBe("initchild1action7child1");
209 | expect(child2.foo).toBe("initchild2action7child2");
210 | });
211 | });
212 |
--------------------------------------------------------------------------------
/test/actions.ts:
--------------------------------------------------------------------------------
1 | import { Module, Mutation, Action, VuexModule } from "../src";
2 | import Vuex, { Payload, MutationPayload } from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | @Module
9 | class MyModule extends VuexModule {
10 | documentId = 0;
11 | text = "";
12 |
13 | get documentHasText() {
14 | return this.documentId > 10;
15 | }
16 |
17 | @Mutation
18 | setDocumentId(id: number) {
19 | this.documentId = id;
20 | }
21 | @Mutation
22 | setText(text: string) {
23 | this.text = text;
24 | }
25 |
26 | @Action
27 | async dummyAction(payload: any) {
28 | // to test accessor
29 | }
30 |
31 | @Action
32 | async loadText(documentId: number) {
33 | if (this.documentId === 0) {
34 | this.setDocumentId(documentId);
35 |
36 | if (this.documentHasText) {
37 | const text = await Promise.resolve("some other text");
38 | this.setText(text);
39 | }
40 | }
41 | }
42 | }
43 |
44 | const myModule = new MyModule({ store, name: "myModule" });
45 |
46 | interface ActionPayload extends Payload {
47 | payload?: any;
48 | }
49 |
50 | describe("actions", () => {
51 | test("accessor dispatches action", async () => {
52 | // subscribeAction missing from vuex typings
53 | const actionObserver = jest.fn((action: ActionPayload) => action);
54 | (store as any).subscribeAction(actionObserver);
55 |
56 | await myModule.dummyAction(5);
57 |
58 | expect(actionObserver.mock.calls.length).toBe(1);
59 |
60 | const mutationPayload = actionObserver.mock.results[0].value as ActionPayload;
61 | expect(mutationPayload.type).toBe("myModule/dummyAction");
62 | expect(mutationPayload.payload).toBe(5);
63 | });
64 |
65 | test("'this' matches vuex context", async () => {
66 | const mutationObserver = jest.fn((mutation: MutationPayload) => mutation);
67 | store.subscribe(mutationObserver);
68 |
69 | await store.dispatch("myModule/loadText", 11);
70 |
71 | expect(mutationObserver.mock.calls.length).toBe(2);
72 |
73 | const firstMutation = mutationObserver.mock.results[0].value as MutationPayload;
74 | expect(firstMutation.type).toBe("myModule/setDocumentId");
75 | expect(firstMutation.payload).toBe(11);
76 |
77 | const secondMutation = mutationObserver.mock.results[1].value as MutationPayload;
78 | expect(secondMutation.type).toBe("myModule/setText");
79 | expect(secondMutation.payload).toBe("some other text");
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/test/constructor.ts:
--------------------------------------------------------------------------------
1 | import { Module, VuexModule, RegisterOptions } from "../src";
2 | import Vuex from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | @Module
9 | class MyModule extends VuexModule {
10 | foo: string;
11 |
12 | constructor(foo: string, options: RegisterOptions) {
13 | super(options);
14 | this.foo = foo;
15 | }
16 | }
17 |
18 | test("constructor", () => {
19 | const myModule = new MyModule("bar", { store, name: "myModule" });
20 | expect(myModule.foo).toBe("bar");
21 | });
22 |
--------------------------------------------------------------------------------
/test/generate-mutations.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex, { MutationPayload } from "vuex";
3 | import { Action, Module, VuexModule } from "../src";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | @Module({ generateMutationSetters: true })
9 | class MyModule extends VuexModule {
10 | id = 0;
11 | text = "";
12 |
13 | @Action
14 | async loadData() {
15 | const { id, text } = await Promise.resolve({ id: 1, text: "some text" });
16 | this.id = id;
17 | this.text = text;
18 | }
19 | }
20 |
21 | const myModule = new MyModule({ store, name: "myModule" });
22 |
23 | test("generate-mutations", async () => {
24 | const mutationObserver = jest.fn((mutation: MutationPayload) => mutation);
25 | store.subscribe(mutationObserver);
26 |
27 | await myModule.loadData();
28 |
29 | const firstMutation = mutationObserver.mock.results[0].value as MutationPayload;
30 | expect(firstMutation.type).toBe("myModule/set__id");
31 | expect(firstMutation.payload).toBe(1);
32 |
33 | const secondMutation = mutationObserver.mock.results[1].value as MutationPayload;
34 | expect(secondMutation.type).toBe("myModule/set__text");
35 | expect(secondMutation.payload).toBe("some text");
36 |
37 | // change state directly using generated mutation
38 | myModule.text = "some other text";
39 |
40 | const thirdMutation = mutationObserver.mock.results[2].value as MutationPayload;
41 | expect(thirdMutation.type).toBe("myModule/set__text");
42 | expect(thirdMutation.payload).toBe("some other text");
43 | });
44 |
--------------------------------------------------------------------------------
/test/getters-inheritance.ts:
--------------------------------------------------------------------------------
1 | import { Action, Module, Mutation, VuexModule } from "../src";
2 | import Vuex, { Store } from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 |
7 | class ParentModule extends VuexModule {
8 | foo = "bar";
9 |
10 | get bigFoo() {
11 | return this.foo.toUpperCase();
12 | }
13 |
14 | get snakeFoo() {
15 | return "__";
16 | }
17 |
18 | @Mutation
19 | myMutation(value: string) {
20 | this.foo = value;
21 | }
22 |
23 | @Action
24 | myAction() {
25 | if (this.bigFoo === "BAR") {
26 | this.myMutation("ok");
27 | }
28 | if (this.bigFoo === "BAZ") {
29 | this.myMutation("alright");
30 | }
31 | }
32 | }
33 |
34 | @Module
35 | class Module1 extends ParentModule {
36 | private tag = "child1";
37 |
38 | get snakeFoo() {
39 | return `_${this.foo}_${this.tag}_`;
40 | }
41 | }
42 |
43 | @Module
44 | class Module2 extends ParentModule {
45 | private tag = "child2";
46 | foo = "baz";
47 |
48 | get snakeFoo() {
49 | return `_${this.foo}_${this.tag}_`;
50 | }
51 | }
52 |
53 | describe("getters-inheritance", () => {
54 | let store: Store;
55 | let child1: Module1;
56 | let child2: Module2;
57 |
58 | beforeEach(() => {
59 | store = new Vuex.Store({});
60 | child1 = new Module1({ store, name: "child1" });
61 | child2 = new Module2({ store, name: "child2" });
62 | });
63 |
64 | test("allows the use of getters from an inheriting class", () => {
65 | expect(child1.bigFoo).toBe("BAR");
66 | expect(child1.bigFoo).toBe(store.getters["child1/bigFoo"]);
67 | expect(child2.bigFoo).toBe("BAZ");
68 | expect(child2.bigFoo).toBe(store.getters["child2/bigFoo"]);
69 | });
70 |
71 | test("allows the use of getters in inherited class", () => {
72 | child1.myAction();
73 | child2.myAction();
74 | expect(child1.bigFoo).toBe("OK");
75 | expect(child1.bigFoo).toBe(store.getters["child1/bigFoo"]);
76 | expect(child2.bigFoo).toBe("ALRIGHT");
77 | expect(child2.bigFoo).toBe(store.getters["child2/bigFoo"]);
78 | });
79 |
80 | test("overriden getters behave as expected", () => {
81 | expect(child1.snakeFoo).toBe("_bar_child1_");
82 | expect(child1.snakeFoo).toBe(store.getters["child1/snakeFoo"]);
83 | expect(child2.snakeFoo).toBe("_baz_child2_");
84 | expect(child2.snakeFoo).toBe(store.getters["child2/snakeFoo"]);
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/test/getters.ts:
--------------------------------------------------------------------------------
1 | import { Module, VuexModule } from "../src";
2 | import Vuex from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | @Module
9 | class MyModule extends VuexModule {
10 | foo = {
11 | text: "some text"
12 | };
13 |
14 | get textTransforms() {
15 | return {
16 | original: this.foo.text,
17 | upperCase: this.foo.text.toUpperCase()
18 | };
19 | }
20 | }
21 |
22 | const myModule = new MyModule({ store, name: "myModule" });
23 |
24 | test("getters", () => {
25 | expect(myModule.textTransforms).toBe(store.getters["myModule/textTransforms"]);
26 | expect(myModule.textTransforms.upperCase).toBe("SOME TEXT");
27 | });
28 |
--------------------------------------------------------------------------------
/test/instanceof.ts:
--------------------------------------------------------------------------------
1 | import { Module, VuexModule } from "../src";
2 | import Vuex from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | class OriginalModule extends VuexModule {
9 | foo = {
10 | text: "some text"
11 | };
12 | bar = 1;
13 | }
14 |
15 | /** Manually apply decorator, to have access to initial class definition */
16 | const MyModule = Module(OriginalModule);
17 | const myModule = new MyModule({ store, name: "myModule" });
18 |
19 | test("instance of", () => {
20 | expect(myModule instanceof OriginalModule).toBe(true);
21 | expect(myModule instanceof MyModule).toBe(true);
22 | expect(myModule instanceof VuexModule).toBe(true);
23 | });
24 |
--------------------------------------------------------------------------------
/test/local-functions-inheritance.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex, { Store } from "vuex";
3 | import { Action, Module, Mutation, VuexModule } from "../src";
4 |
5 | Vue.use(Vuex);
6 |
7 | abstract class Parent extends VuexModule {
8 | canTransform = true;
9 | text = "parent text";
10 |
11 | get upperCaseText() {
12 | return this.text.toUpperCase();
13 | }
14 |
15 | get noSpaceText() {
16 | return this.snakeText();
17 | }
18 |
19 | private snakeText() {
20 | // 'this' has access to state & getters
21 | return this.canTransform ? this.upperCaseText.replace(/ /g, "_") : "";
22 | }
23 |
24 | @Mutation
25 | setText(text: string) {
26 | this.localSetText(text);
27 | }
28 |
29 | @Mutation
30 | clearText() {
31 | this.text = "";
32 | }
33 |
34 | protected abstract localSetText(text: string): void;
35 | protected localLoadText() {
36 | this.setText("parent: yet another text");
37 | }
38 | }
39 |
40 | @Module
41 | class Module1 extends Parent {
42 | private tag = "child1";
43 | canTransform = true;
44 | text = `${this.tag} text`;
45 |
46 | get upperCaseText() {
47 | return this.text.toUpperCase();
48 | }
49 |
50 | protected localSetText(text: string) {
51 | // 'this' has state
52 | this.text = `${text} ${this.tag}`;
53 | }
54 |
55 | @Action
56 | async loadText() {
57 | this.localLoadText();
58 | }
59 |
60 | protected localLoadText() {
61 | // 'this' has getters & mutations
62 | if (!this.upperCaseText) {
63 | this.setText(`${this.tag.toUpperCase()}: yet another text`);
64 | }
65 | }
66 | }
67 |
68 | @Module
69 | class Module2 extends Parent {
70 | private tag = "child2";
71 | canTransform = true;
72 | text = `${this.tag} text`;
73 |
74 | get upperCaseText() {
75 | return this.text.toUpperCase();
76 | }
77 | get noSpaceText() {
78 | return this.dashText();
79 | }
80 |
81 | private dashText() {
82 | // 'this' has access to state & getters
83 | return this.canTransform ? this.upperCaseText.replace(/ /g, "--") : "";
84 | }
85 |
86 | protected localSetText(text: string) {
87 | // 'this' has state
88 | this.text = `***${text}*** ${this.tag}`;
89 | }
90 |
91 | @Action
92 | async loadText() {
93 | this.localLoadText();
94 | }
95 |
96 | protected localLoadText() {
97 | // 'this' has getters & mutations
98 | if (!this.upperCaseText) {
99 | this.setText(`${this.tag}: yet another text`);
100 | }
101 | }
102 | }
103 |
104 | describe("local-functions", () => {
105 | let child1: Module1;
106 | let child2: Module2;
107 | let store: Store;
108 |
109 | beforeEach(() => {
110 | store = new Vuex.Store({});
111 | child1 = new Module1({ store, name: "myModule1" });
112 | child2 = new Module2({ store, name: "myModule2" });
113 | });
114 |
115 | test("from getter", () => {
116 | expect(child1.noSpaceText).toBe("CHILD1_TEXT");
117 | expect(child2.noSpaceText).toBe("CHILD2--TEXT");
118 | });
119 |
120 | test("from mutation", () => {
121 | child1.setText("some other text");
122 | child2.setText("some other text");
123 | expect(child1.text).toBe("some other text child1");
124 | expect(child2.text).toBe("***some other text*** child2");
125 | });
126 |
127 | test("from action", async () => {
128 | child1.clearText();
129 | child2.clearText();
130 |
131 | await child1.loadText();
132 | await child2.loadText();
133 | expect(child1.text).toBe("CHILD1: yet another text child1");
134 | expect(child2.text).toBe("***child2: yet another text*** child2");
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/test/local-functions.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex from "vuex";
3 | import { Action, Module, Mutation, VuexModule } from "../src";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | @Module
9 | class MyModule extends VuexModule {
10 | canTransform = true;
11 | text = "some text";
12 |
13 | get upperCaseText() {
14 | return this.text.toUpperCase();
15 | }
16 | get pascalText() {
17 | return this.transformText();
18 | }
19 | private transformText() {
20 | // 'this' has access to state & getters
21 | return this.canTransform ? this.upperCaseText.replace(/ /g, "_") : "";
22 | }
23 |
24 | @Mutation
25 | setText(text: string) {
26 | this.localSetText(text);
27 | }
28 | private localSetText(text: string) {
29 | // 'this' has state
30 | this.text = text;
31 | }
32 |
33 | @Action
34 | async loadText() {
35 | this.localLoadText();
36 | }
37 | private localLoadText() {
38 | // 'this' has getters & mutations
39 | if (!this.upperCaseText) {
40 | this.setText("yet another text");
41 | }
42 | }
43 | }
44 |
45 | const myModule = new MyModule({ store, name: "myModule" });
46 |
47 | describe("local-functions", () => {
48 | test("from getter", () => {
49 | expect(myModule.pascalText).toBe("SOME_TEXT");
50 | });
51 |
52 | test("from mutation", () => {
53 | myModule.setText("some other text");
54 | expect(myModule.text).toBe("some other text");
55 | });
56 |
57 | test("from action", async () => {
58 | myModule.setText("");
59 |
60 | await myModule.loadText();
61 | expect(myModule.text).toBe("yet another text");
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/test/module-reference.ts:
--------------------------------------------------------------------------------
1 | import { Module, VuexModule, RegisterOptions } from "../src";
2 | import Vuex from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | @Module
9 | class MyModule extends VuexModule {
10 | foo = "bar";
11 | }
12 |
13 | @Module
14 | class OtherModule extends VuexModule {
15 | private myModule: MyModule;
16 |
17 | get moduleRef() {
18 | return this.myModule;
19 | }
20 |
21 | constructor(myModule: MyModule, options: RegisterOptions) {
22 | super(options);
23 | this.myModule = myModule;
24 | }
25 | }
26 |
27 | test("module references", () => {
28 | const myModule = new MyModule({ store, name: "myModule" });
29 | const otherModule = new OtherModule(myModule, { store, name: "otherModule" });
30 | expect(store.state.otherModule.myModule).toBeUndefined();
31 | expect(otherModule.moduleRef).toBe(myModule);
32 | });
33 |
--------------------------------------------------------------------------------
/test/mutations-inheritance.ts:
--------------------------------------------------------------------------------
1 | import { Module, Mutation, VuexModule } from "../src";
2 | import Vuex, { Store } from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 |
7 | class ParentModule extends VuexModule {
8 | foo = "init";
9 |
10 | @Mutation
11 | mutation1(value: string) {
12 | //
13 | }
14 |
15 | @Mutation
16 | mutation2(value: string) {
17 | //
18 | }
19 |
20 | @Mutation
21 | mutation3(value: string) {
22 | this.foo = value;
23 | }
24 | }
25 |
26 | @Module
27 | class Module1 extends ParentModule {
28 | private tag = "child1";
29 | baz = "init" + this.tag;
30 |
31 | @Mutation
32 | mutation1(value: string) {
33 | this.baz = value + this.tag;
34 | }
35 |
36 | @Mutation
37 | mutation2(value: string) {
38 | this.foo = value + this.tag;
39 | }
40 |
41 | @Mutation
42 | mutation3(value: string) {
43 | super.mutation3(value);
44 | this.foo = this.foo + this.tag;
45 | }
46 | }
47 |
48 | @Module
49 | class Module2 extends ParentModule {
50 | private tag = "child2";
51 | bar = "init" + this.tag;
52 | baz = "init" + this.tag;
53 |
54 | @Mutation
55 | mutation1(value: string) {
56 | this.bar = value + this.tag;
57 | }
58 |
59 | @Mutation
60 | mutation2(value: string) {
61 | this.baz = value + this.tag;
62 | }
63 |
64 | @Mutation
65 | mutation3(value: string) {
66 | super.mutation3(value);
67 | this.foo = this.foo + this.tag;
68 | }
69 | }
70 |
71 | describe("mutations-inheritance", () => {
72 | let store: Store;
73 | let parent: ParentModule;
74 | let child1: Module1;
75 | let child2: Module2;
76 |
77 | beforeEach(() => {
78 | store = new Vuex.Store({});
79 | parent = new ParentModule({ store, name: "parentModule" });
80 | child1 = new Module1({ store, name: "myModule1" });
81 | child2 = new Module2({ store, name: "myModule2" });
82 | });
83 |
84 | test("overriden mutation can modify state", () => {
85 | parent.mutation1("_");
86 | child1.mutation1("bar1");
87 | child2.mutation1("bar2");
88 | expect(parent.foo).toBe("init");
89 | expect(child1.baz).toBe("bar1child1");
90 | expect(child2.bar).toBe("bar2child2");
91 | expect(child2.baz).toBe("initchild2");
92 | });
93 |
94 | test("overriden mutation can modify parent state", () => {
95 | parent.mutation2("_");
96 | child1.mutation2("bar");
97 | child2.mutation2("baz");
98 | expect(parent.foo).toBe("init");
99 | expect(child1.foo).toBe("barchild1");
100 | expect(child2.bar).toBe("initchild2");
101 | expect(child2.baz).toBe("bazchild2");
102 | });
103 |
104 | test("overriden mutation has access to parent method implementation", () => {
105 | parent.mutation3("foo_");
106 | child1.mutation3("foo1");
107 | child2.mutation3("foo2");
108 | expect(parent.foo).toBe("foo_");
109 | expect(child1.foo).toBe("foo1child1");
110 | expect(child1.baz).toBe("initchild1");
111 | expect(child2.foo).toBe("foo2child2");
112 | expect(child2.bar).toBe("initchild2");
113 | expect(child2.baz).toBe("initchild2");
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/test/mutations.ts:
--------------------------------------------------------------------------------
1 | import { Module, Mutation, VuexModule } from "../src";
2 | import Vuex, { MutationPayload } from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | @Module
9 | class MyModule extends VuexModule {
10 | shouldUpdate = true;
11 | text = "";
12 |
13 | @Mutation
14 | setText(text: string) {
15 | if (this.shouldUpdate) {
16 | this.text = text;
17 | }
18 | }
19 | }
20 |
21 | const myModule = new MyModule({ store, name: "myModule" });
22 |
23 | describe("mutations", () => {
24 | test("accessor calls commit", () => {
25 | const mutationObserver = jest.fn((mutation: MutationPayload) => mutation);
26 | store.subscribe(mutationObserver);
27 |
28 | myModule.setText("some text");
29 |
30 | expect(mutationObserver.mock.calls.length).toBe(1);
31 |
32 | const mutationPayload = mutationObserver.mock.results[0].value as MutationPayload;
33 | expect(mutationPayload.type).toBe("myModule/setText");
34 | expect(mutationPayload.payload).toBe("some text");
35 | });
36 |
37 | test("updates store", () => {
38 | store.commit("myModule/setText", "some other text");
39 | expect(store.state.myModule.text).toBe("some other text");
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/test/state.ts:
--------------------------------------------------------------------------------
1 | import { Module, VuexModule } from "../src";
2 | import Vuex from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | @Module
9 | class MyModule extends VuexModule {
10 | foo = {
11 | text: "some text"
12 | };
13 | bar = 1;
14 |
15 | square = (num: number) => num * num;
16 | }
17 |
18 | const myModule = new MyModule({ store, name: "myModule" });
19 |
20 | test("state", () => {
21 | expect(myModule.foo).toBe(store.state.myModule.foo);
22 | expect(myModule.foo.text).toBe("some text");
23 |
24 | expect(myModule.bar).toBe(store.state.myModule.bar);
25 | expect(myModule.bar).toBe(1);
26 |
27 | expect(myModule.square).toBe(store.state.myModule.square);
28 | expect(myModule.square(2)).toBe(4);
29 | });
30 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "",
5 | "experimentalDecorators": true,
6 | "declaration": false
7 | },
8 | "include": [
9 | "./**/*.ts"
10 | ]
11 | }
--------------------------------------------------------------------------------
/test/watch.ts:
--------------------------------------------------------------------------------
1 | import { Module, Mutation, Action, VuexModule } from "../src";
2 | import Vuex from "vuex";
3 | import Vue from "vue";
4 |
5 | Vue.use(Vuex);
6 | const store = new Vuex.Store({});
7 |
8 | @Module
9 | class MyModule extends VuexModule {
10 | text = "";
11 |
12 | get getText() {
13 | return this.text;
14 | }
15 |
16 | @Mutation
17 | setText(text: string) {
18 | this.text = text;
19 | }
20 |
21 | @Action
22 | async changeText(text: string) {
23 | this.setText(text);
24 | }
25 | }
26 |
27 | const myModule = new MyModule({ store, name: "myModule" });
28 |
29 | describe("watch", () => {
30 | test("watch callback is called", async () => {
31 | const watchCallback = jest.fn((newValue: string, oldValue: string) => undefined);
32 |
33 | myModule.setText("bar");
34 | myModule.$watch(theModule => theModule.getText, watchCallback);
35 | await myModule.changeText("foo");
36 |
37 | expect(watchCallback.mock.calls.length).toBe(1);
38 | expect(watchCallback.mock.calls[0].length).toBe(2);
39 | expect(watchCallback.mock.calls[0][0]).toBe("foo");
40 | expect(watchCallback.mock.calls[0][1]).toBe("bar");
41 | });
42 |
43 | test("watch for state changes as well", async () => {
44 | const watchCallback = jest.fn((newValue: string, oldValue: string) => undefined);
45 |
46 | myModule.setText("bar");
47 | myModule.$watch(theModule => theModule.text, watchCallback);
48 | await myModule.changeText("foo");
49 |
50 | expect(watchCallback.mock.calls.length).toBe(1);
51 | expect(watchCallback.mock.calls[0].length).toBe(2);
52 | expect(watchCallback.mock.calls[0][0]).toBe("foo");
53 | expect(watchCallback.mock.calls[0][1]).toBe("bar");
54 | });
55 |
56 | test("watch should return unwatch func", async () => {
57 | const watchCallback = jest.fn((newValue: string, oldValue: string) => undefined);
58 |
59 | myModule.setText("bar");
60 | const unwatch = myModule.$watch(theModule => theModule.text, watchCallback);
61 | await myModule.changeText("foo1");
62 |
63 | expect(watchCallback.mock.calls.length).toBe(1);
64 |
65 | unwatch();
66 | await myModule.changeText("foo2");
67 |
68 | expect(watchCallback.mock.calls.length).toBe(1);
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "es2015",
5 | "moduleResolution": "node",
6 | "lib": [
7 | "dom",
8 | "es2015"
9 | ],
10 | "declaration": true,
11 | "outDir": "./lib",
12 | "strict": true,
13 |
14 | "experimentalDecorators": true
15 | },
16 | "include": ["src"],
17 | "exclude": ["node_modules", "**/__tests__/*"]
18 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint:recommended",
4 | "tslint-config-prettier"
5 | ],
6 | "rulesDirectory": ["tslint-plugin-prettier"],
7 | "rules": {
8 | "prettier": [true, { "printWidth": 120 }],
9 |
10 | "no-namespace": false,
11 | "max-line-length": false,
12 | "interface-name": false,
13 |
14 | "arrow-parens": [false],
15 | "object-literal-sort-keys": false,
16 | "ordered-imports": [
17 | false
18 | ],
19 | "member-access": [
20 | true, "no-public"
21 | ],
22 | "max-classes-per-file": false,
23 | "trailing-comma": [
24 | false
25 | ],
26 | "interface-over-type-literal": false,
27 | "no-console": [false],
28 | "one-line": false,
29 | "curly": false,
30 | "no-empty": [true, "allow-empty-catch", "allow-empty-functions"],
31 | "member-ordering": false,
32 | "no-unused-expression": false,
33 | "only-arrow-functions": false,
34 | "ban-types": [true, "Function"],
35 | "variable-name": false
36 | },
37 | "defaultSeverity": "warning"
38 | }
--------------------------------------------------------------------------------