Repository: gertqin/vuex-class-modules
Branch: master
Commit: c5d19b483358
Files: 41
Total size: 54.5 KB
Directory structure:
gitextract_ot546s4y/
├── .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.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
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules
/lib
/commonjs
/example/build.js
/example/build.js.map
================================================
FILE: .vscode/settings.json
================================================
{
"editor.rulers": [ 120 ],
"prettier.printWidth": 120,
"prettier.disableLanguages": [],
"[typescript]": {
"editor.formatOnSave": true
},
"[vue]": {
"editor.formatOnSave": true
},
"tslint.autoFixOnSave": true,
"vetur.format.defaultFormatter.html": "none",
"vetur.format.defaultFormatter.ts": "none"
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 gertqin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# vuex-class-modules
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).
[](https://www.npmjs.com/package/vuex-class-modules)
## Installation
`npm install vuex-class-modules`
And make sure to have the `--experimentalDecorators` flag enabled.
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.
## Usage
Vuex modules can be written using decorators as a class:
```typescript
// user-module.ts
import { VuexModule, Module, Mutation, Action } from "vuex-class-modules";
@Module
class UserModule extends VuexModule {
// state
firstName = "Foo";
lastName = "Bar";
// getters
get fullName() {
return this.firstName + " " + this.lastName;
}
// mutations
@Mutation
setFirstName(firstName: string) {
this.firstName = firstName;
}
@Mutation
setLastName(lastName: string) {
this.lastName = lastName;
}
// actions
@Action
async loadUser() {
const user = await fetchUser();
this.setFirstName(user.firstName);
this.setLastName(user.lastName);
}
}
// register module (could be in any file)
import store from "path/to/store";
export const userModule = new UserModule({ store, name: "user" });
```
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.)
The module can then be used in vue components as follows:
```ts
// MyComponent.vue
import Vue from "vue";
import { userModule } from "path/to/user-module.ts";
export class MyComponent extends Vue {
get firstName() {
return userModule.firstName; // -> store.state.user.firstName
}
get fullName() {
return userModule.fullName; // -> store.getters["user/fullName]
}
created() {
userModule.setFirstName("Foo"); // -> store.commit("user/setFirstName", "Foo")
userModule.loadUser(); // -> store.dispatch("user/loadUser")
}
}
```
### What about `rootState` and `rootGetters`?
There are two ways to access other modules within a module, or dispatch actions to other modules.
1. Simply import the instantiated module (suitable if the modules are instantiated in the same file as they are defined):
```ts
// my-module.ts
// import the module instance
import { otherModule } from "./other-module";
@Module
class MyModule extends VuexModule {
get myGetter() {
return otherModule.foo;
}
@Action
async myAction() {
await otherModule.someAction();
// ...
}
}
```
2. The other module can be registered through the constructor (suitable if the modules are instantiated elsewhere)
```ts
// my-module.ts
// import the class, not the instance
import { OtherModule } from "./other-module";
@Module
export class MyModule extends VuexModule {
private otherModule: OtherModule;
constructor(otherModule: OtherModule, options: RegisterOptions) {
super(options);
this.otherModule = otherModule;
}
get myGetter() {
return this.otherModule.foo;
}
@Action
async myAction() {
await this.otherModule.someAction();
// ...
}
}
// register-modules.ts
import store from "path/to/store";
import { OtherModule } from "path/to/other-module";
import { MyModule } from "path/to/my-module";
export const otherModule = new OtherModule({ store, name: "otherModule" });
export const myModule = new MyModule(otherModule, { store, name: "myModule" });
```
The local modules will not be part of the state and cannot be accessed from the outside, so they should always be declared private.
```ts
myModule.otherModule; // -> undefined
```
### The `store.watch` function
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).
This api is also provided by vuex-class-modules under the method name `$watch` to prevent name collisions. For example you can do:
```ts
import store from "./store";
import { MyModule } from "./my-module";
const myModule = new MyModule({ store, name: "MyModule" });
myModule.$watch(
(theModule) => theModule.fullName,
(newName: string, oldName: string) => {
// ...
},
{
deep: false,
immediate: false,
}
);
```
and to unwatch:
```ts
const unwatch = myModule.$watch(...);
unwatch();
```
### Register options
- `name` [required]: Name of the module
- `store` [required]: The vuex store - which can just be instantiated as empty:
```ts
// store.ts
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const store = new Vuex.Store({});
```
### Module options
The module decorator can also accept options:
- `generateMutationSetters` [optional, default=false]: Whether automatic mutation setters for the state properties should be generated, see [Generate Mutation Setters](#generate-mutation-setters).
## Example
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:
`npm run example`
## Caveats of `this`
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:
```ts
@Module
class MyModule extends VuexModule {
foo = "bar";
get someGetter() {
return 123;
}
get myGetter() {
this.foo; // -> "bar"
this.someGetter; // -> 123
this.someMutation(); // undefined, getters cannot call mutations
this.someAction(); // -> undefined, getters cannot call actions
}
@Mutation
someMutation() {
/* ... */
}
@Mutation
myMutation() {
this.foo; // -> "bar"
this.someGetter; // -> undefined, mutations dont have access to getters
this.someMutation(); // -> undefined, mutations cannot call other mutations
this.someAction(); // -> undefined, mutations cannot call actions
}
@Action
async someAction() {
/* ... */
}
@Action
async myAction() {
this.foo; // -> "bar"
this.someGetter; // -> 123
this.myMutation(); // Ok
await this.someAction(); // Ok
}
}
```
## Local Functions
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.
```ts
@Module
class MyModule extends VuexModule {
get myGetter() {
return myGetterHelper();
}
private myGetterHelper() {
// same 'this' context as myGetter
}
@Mutation
myMutation() {
this.myMutationHelper();
}
// should be private
myMutationHelper() { /* ... */}
}
const myModule = new MyModule({ store, name: "myModule });
myModule.myMutationHelper // -> undefined.
```
## Generate Mutation Setters
As I often find myself writing a lot of simple setter mutations like
```ts
@Module
class UserModule extends VuexModule {
firstName = "Foo";
lastName = "Bar";
@Mutation
setFirstName(firstName: string) {
this.firstName = firstName;
}
@Mutation
setLastName(lastName: string) {
this.lastName = lastName;
}
}
```
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:
```ts
@Module({ generateMutationSetters: true })
class UserModule extends VuexModule {
firstName = "Foo";
lastName = "Bar";
// Auto generated:
// @Mutation set__firstName(val: any) { this.firstName = val }
// @Mutation set__lastName(val: any) { this.lastName = val }
@Action
async loadUser() {
const user = await fetchUser();
this.firstName = user.firstName; // -> this.set__firstName(user.firstName);
this.lastName = user.lastName; // -> this.set__lastName(user.lastName);
}
}
```
_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:
```ts
@Module({ generateMutationSetters: true })
class UserModule extends VuexModule {
user = {
id: 123,
name: "Foo",
};
@Mutation
setUserName() {
this.user.name = "Bar"; // OK!
}
@Action
async loadUser() {
this.user.name = "Bar"; // Bad, the state is mutated outside a mutation
this.user = { ...this.user, name: "Bar" }; // OK!
}
}
```
## Vite HMR
[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
```ts
VuexModule.__useHotUpdate = true; // default false
```
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
```ts
VuexModule.__useHotUpdate = import.meta.env.DEV;
```
## License
[MIT](http://opensource.org/licenses/MIT)
================================================
FILE: example/index.html
================================================
Vuex Class Modules Example
================================================
FILE: example/src/api/shop.ts
================================================
/**
* Mocking client-server processing
*/
export interface Product {
id: number;
title: string;
price: number;
inventory: number;
}
export interface CartItem {
id: number;
quantity: number;
}
const products: Product[] = [
{ id: 1, title: "iPad 4 Mini", price: 500.01, inventory: 2 },
{ id: 2, title: "H&M T-Shirt White", price: 10.99, inventory: 10 },
{ id: 3, title: "Charli XCX - Sucker CD", price: 19.99, inventory: 5 }
];
export default {
async getProducts() {
await new Promise(resolve => setTimeout(resolve, 100));
return products;
},
async buyProducts(items: CartItem[]) {
await new Promise(resolve => setTimeout(resolve, 100));
if (Math.random() > 0.5) {
throw Error();
}
}
};
================================================
FILE: example/src/app.ts
================================================
import Vue from "vue";
import App from "./components/App.vue";
import store from "./store";
new Vue({
el: "#app",
store,
render: h => h(App)
});
================================================
FILE: example/src/components/App.vue
================================================
Shopping Cart Example
Products
================================================
FILE: example/src/components/ProductList.vue
================================================
{{ product.title }} - {{ product.price }}€
Add to cart
================================================
FILE: example/src/components/ShoppingCart.vue
================================================
Your Cart
Please add some products to cart.
{{ product.title }} - {{ product.price }}€ x {{ product.quantity }}
Total: {{ total }}€
Checkout
Checkout {{ checkoutStatus }}.
================================================
FILE: example/src/shims-vue.d.ts
================================================
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
================================================
FILE: example/src/store/cart.ts
================================================
import store from "./";
import shop, { CartItem, Product } from "../api/shop";
import { Module, Mutation, Action, VuexModule } from "../../../lib/index";
import { productsModule } from "./products";
@Module({ generateMutationSetters: true })
class Cart extends VuexModule {
items: CartItem[] = [];
checkoutStatus = "";
get cartProducts() {
return this.items.map(({ id, quantity }) => {
const product = productsModule.all.find(p => p.id === id);
return {
title: product!.title,
price: product!.price,
quantity
};
});
}
get cartTotalPrice() {
return this.cartProducts.reduce((total, product) => {
return total + product.price * product.quantity;
}, 0);
}
@Mutation
pushProductToCart(id: number) {
this.items.push({
id,
quantity: 1
});
}
@Mutation
incrementItemQuantity(id: number) {
const cartItem = this.items.find(item => item.id === id);
cartItem!.quantity++;
}
@Action
async addProductToCart(product: Product) {
this.checkoutStatus = "";
if (product.inventory > 0) {
const cartItem = this.items.find(item => item.id === product.id);
if (!cartItem) {
this.pushProductToCart(product.id);
} else {
this.incrementItemQuantity(cartItem.id);
}
// remove 1 item from stock
productsModule.decrementProductInventory(product.id);
}
}
@Action
async checkout() {
const savedCartItems = [...this.items];
this.checkoutStatus = "";
// empty cart
this.items = [];
try {
await shop.buyProducts(savedCartItems);
this.checkoutStatus = "successful";
} catch (e) {
this.items = savedCartItems;
this.checkoutStatus = "failed";
}
}
}
export const cartModule = new Cart({ store, name: "cart" });
================================================
FILE: example/src/store/index.ts
================================================
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({});
================================================
FILE: example/src/store/products.ts
================================================
import store from "./";
import shop, { Product } from "../api/shop";
import { Module, Mutation, Action, VuexModule } from "../../../lib/index";
@Module({ generateMutationSetters: true })
class Products extends VuexModule {
all: Product[] = [];
@Mutation
decrementProductInventory(id: number) {
const product = this.all.find(p => p.id === id);
product!.inventory--;
}
@Action
async getAllProducts() {
this.all = await shop.getProducts();
}
}
export const productsModule = new Products({ store, name: "products" });
================================================
FILE: example/tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"lib": [
"dom",
"esnext"
],
"module": "es2015",
"moduleResolution": "node",
"experimentalDecorators": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": false
},
"include": [
"./**/*.ts",
]
}
================================================
FILE: example/webpack.config.js
================================================
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
mode: 'development',
context: __dirname,
entry: './src/app.ts',
output: {
path: __dirname,
filename: 'build.js'
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
appendTsxSuffixTo: [/\.vue$/]
}
}
]
},
{
test: /\.vue$/,
use: ['vue-loader']
}
]
},
devtool: 'source-map',
plugins: [
new VueLoaderPlugin()
]
}
================================================
FILE: jestconfig.json
================================================
{
"preset": "ts-jest",
"rootDir": "./test",
"testMatch": ["/**/*.ts"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
"globals": {
"ts-jest": {
"tsConfig": {
"isolatedModules": false,
"esModuleInterop": true
}
}
}
}
================================================
FILE: package.json
================================================
{
"name": "vuex-class-modules",
"version": "1.3.0",
"description": "Typescript class decorators for class-style vuex modules.",
"main": "commonjs/index.js",
"module": "lib/index.js",
"exports": {
".": {
"require": "./commonjs/index.js",
"default": "./lib/index.js"
}
},
"types": "lib/index.d.ts",
"files": [
"lib",
"commonjs"
],
"scripts": {
"test": "jest --config jestconfig.json",
"build": "npm run build:es2015 && npm run build:commonjs",
"build:es2015": "tsc",
"build:commonjs": "tsc -m commonjs --outDir ./commonjs",
"lint": "tslint -p tsconfig.json --fix",
"example": "npm run build && webpack --config ./example/webpack.config.js",
"prepare": "npm run build",
"prepublishOnly": "npm test && npm run lint",
"preversion": "npm run lint",
"version": "git add -A src",
"postversion": "git push && git push --tags"
},
"repository": {
"type": "git",
"url": "git+https://github.com/gertqin/vuex-class-modules.git"
},
"keywords": [
"vue",
"vuex",
"typescript",
"class",
"decorators"
],
"author": "Gert Qin Hansen",
"license": "MIT",
"bugs": {
"url": "https://github.com/gertqin/vuex-class-modules/issues"
},
"homepage": "https://github.com/gertqin/vuex-class-modules#readme",
"devDependencies": {
"@types/node": "12.12.2",
"@types/jest": "^26.0.14",
"@types/webpack-env": "^1.13.9",
"css-loader": "^3.2.0",
"jest": "^26.4.0",
"ts-jest": "^26.4.0",
"ts-loader": "^6.2.0",
"tslint": "^5.20.0",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.8.4",
"vue": "^2.6.10",
"vue-class-component": "^7.1.0",
"vue-loader": "^15.7.0",
"vue-template-compiler": "^2.6.10",
"vuex": "^3.1.1",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
}
}
================================================
FILE: src/VuexModule.ts
================================================
import { RegisterOptions } from "./module-factory";
import { WatchOptions } from "vue";
export class VuexModule {
private __options: RegisterOptions;
static __useHotUpdate: boolean = false;
constructor(options: RegisterOptions) {
this.__options = options;
}
$watch(fn: (arg: this) => T, callback: (newValue: T, oldValue: T) => void, options?: WatchOptions): Function {
return function() {};
}
}
================================================
FILE: src/actions.ts
================================================
import { ModulePrototype } from "./module-factory";
export function Action(
target: T,
key: string | symbol,
descriptor: TypedPropertyDescriptor<(arg?: any) => any>
) {
const vuexModule = target.constructor as ModulePrototype;
if (!vuexModule.__actions) {
vuexModule.__actions = {};
}
if (descriptor.value) {
vuexModule.__actions[key as string] = descriptor.value;
}
}
================================================
FILE: src/index.ts
================================================
export { RegisterOptions, ModuleOptions } from "./module-factory";
export { Action } from "./actions";
export { Module } from "./module";
export { Mutation } from "./mutations";
export { VuexModule } from "./VuexModule";
================================================
FILE: src/module-factory.ts
================================================
import { Store, Module as StoreModule, GetterTree, ActionContext, Dispatch, Commit } from "vuex";
import { WatchOptions } from "vue";
import { VuexModule } from "./VuexModule";
export interface ModuleOptions {
generateMutationSetters?: boolean;
}
export interface RegisterOptions {
store: Store;
name: string;
}
export interface IVuexModule extends Dictionary {
__options: RegisterOptions;
}
export interface IModulePrototype {
__mutations?: Dictionary<(payload?: any) => void>;
__actions?: Dictionary<(payload?: any) => Promise>;
}
export type ModulePrototype = IModulePrototype & Function;
type Dictionary = { [k: string]: T };
interface ModuleDefinition {
state: Dictionary;
moduleRefs: Dictionary;
getters: Dictionary<() => void>;
mutations: Dictionary<(payload?: any) => void>;
actions: Dictionary<(payload?: any) => Promise>;
localFunctions: Dictionary<(...args: any[]) => any>;
}
interface StoreProxyDefinition {
state?: Dictionary;
stateSetter?: (key: string, val: any) => void;
getters?: Dictionary;
commit?: Commit;
dispatch?: Dispatch;
useNamespaceKey?: boolean;
excludeModuleRefs?: boolean;
excludeLocalFunctions?: boolean;
}
export class VuexClassModuleFactory {
moduleOptions: ModuleOptions;
instance: IVuexModule;
registerOptions: RegisterOptions;
definition: ModuleDefinition = {
state: {},
moduleRefs: {},
getters: {},
mutations: {},
actions: {},
localFunctions: {}
};
constructor(classModule: ModulePrototype, instance: IVuexModule, moduleOptions: ModuleOptions) {
this.moduleOptions = moduleOptions;
this.instance = instance;
this.registerOptions = instance.__options;
this.init(classModule);
}
private init(classModule: ModulePrototype) {
// state
for (const key of Object.keys(this.instance)) {
const val = this.instance[key];
if (key !== "__options" && this.instance.hasOwnProperty(key)) {
if (val instanceof VuexModule) {
this.definition.moduleRefs[key] = val;
} else {
this.definition.state[key] = this.instance[key];
}
}
}
const actionKeys = Object.keys(classModule.__actions || {});
const mutationKeys = Object.keys(classModule.__mutations || {});
const isAction = (key: string) => actionKeys.indexOf(key) !== -1;
const isMutation = (key: string) => mutationKeys.indexOf(key) !== -1;
for (const module of getModulePrototypes(classModule)) {
for (const key of Object.getOwnPropertyNames(module.prototype)) {
const descriptor = Object.getOwnPropertyDescriptor(module.prototype, key) as PropertyDescriptor;
const isGetter = !!descriptor.get;
if (isGetter && !(key in this.definition.getters)) {
this.definition.getters[key] = descriptor.get!;
}
if (isAction(key) && !(key in this.definition.actions) && descriptor.value) {
this.definition.actions[key] = module.prototype[key];
}
if (isMutation(key) && !(key in this.definition.mutations) && descriptor.value) {
this.definition.mutations[key] = module.prototype[key];
}
const isHelperFunction =
descriptor.value &&
typeof module.prototype[key] === "function" &&
!isAction(key) &&
!isMutation(key) &&
key !== "constructor";
if (isHelperFunction && !(key in this.definition.localFunctions)) {
this.definition.localFunctions[key] = module.prototype[key];
}
}
}
}
registerVuexModule() {
const vuexModule: StoreModule = {
state: this.definition.state,
getters: {},
mutations: {},
actions: {},
namespaced: true
};
// getters
mapValues(vuexModule.getters!, this.definition.getters, getter => {
return (state: any, getters: GetterTree) => {
const thisObj = this.buildThisProxy({ state, getters });
return getter.call(thisObj);
};
});
// mutations
mapValues(vuexModule.mutations!, this.definition.mutations, mutation => {
return (state: any, payload: any) => {
const thisObj = this.buildThisProxy({
state,
stateSetter: (stateField: string, val: any) => {
state[stateField] = val;
}
});
mutation.call(thisObj, payload);
};
});
if (this.moduleOptions.generateMutationSetters) {
for (const stateKey of Object.keys(this.definition.state)) {
const mutation = (state: any, payload: any) => {
state[stateKey] = payload;
};
vuexModule.mutations![this.getMutationSetterName(stateKey)] = mutation;
}
}
// actions
mapValues(vuexModule.actions!, this.definition.actions, action => {
return (context: ActionContext, payload: any) => {
const proxyDefinition: StoreProxyDefinition = {
...context,
stateSetter: this.moduleOptions.generateMutationSetters
? (field: string, val: any) => {
context.commit(this.getMutationSetterName(field), val);
}
: undefined
};
const thisObj = this.buildThisProxy(proxyDefinition);
return action.call(thisObj, payload);
};
});
// register module
const { store, name } = this.registerOptions;
if (store.state[name]) {
if (VuexModule.__useHotUpdate || (typeof module !== "undefined" && module.hot)) {
store.hotUpdate({
modules: {
[name]: vuexModule
}
});
} else {
throw Error(`[vuex-class-module]: A module with name '${name}' already exists.`);
}
} else {
store.registerModule(this.registerOptions.name, vuexModule);
}
}
buildAccessor() {
const { store, name } = this.registerOptions;
const stateSetter = this.moduleOptions.generateMutationSetters
? (field: string, val: any) => {
store.commit(`${name}/${this.getMutationSetterName(field)}`, val);
}
: undefined;
const accessorModule = this.buildThisProxy({
...store,
state: store.state[name],
stateSetter,
useNamespaceKey: true,
excludeModuleRefs: true,
excludeLocalFunctions: true
});
// watch API
accessorModule.$watch = (
fn: (arg: VuexModule) => any,
callback: (newValue: any, oldValue: any) => void,
options?: WatchOptions
) => {
return store.watch(
(state: any, getters: any) =>
fn(
this.buildThisProxy({
state: state[name],
getters,
useNamespaceKey: true
})
),
callback,
options
);
};
Object.setPrototypeOf(accessorModule, Object.getPrototypeOf(this.instance));
Object.freeze(accessorModule);
return accessorModule;
}
private buildThisProxy(proxyDefinition: StoreProxyDefinition) {
const obj: any = {};
if (proxyDefinition.state) {
mapValuesToProperty(
obj,
this.definition.state,
key => proxyDefinition.state![key],
proxyDefinition.stateSetter
? (key, val) => proxyDefinition.stateSetter!(key, val)
: () => {
throw Error("[vuex-class-module]: Cannot modify state outside mutations.");
}
);
}
if (!proxyDefinition.excludeModuleRefs) {
mapValues(obj, this.definition.moduleRefs, val => val);
}
const namespaceKey = proxyDefinition.useNamespaceKey ? this.registerOptions.name + "/" : "";
if (proxyDefinition.getters) {
mapValuesToProperty(obj, this.definition.getters, key => proxyDefinition.getters![`${namespaceKey}${key}`]);
}
if (proxyDefinition.commit) {
mapValues(obj, this.definition.mutations, (mutation, key) => {
return (payload?: any) => proxyDefinition.commit!(`${namespaceKey}${key}`, payload);
});
}
if (proxyDefinition.dispatch) {
mapValues(obj, this.definition.actions, (action, key) => {
return (payload?: any) => proxyDefinition.dispatch!(`${namespaceKey}${key}`, payload);
});
}
if (!proxyDefinition.excludeLocalFunctions) {
mapValues(obj, this.definition.localFunctions, localFunction => {
return (...args: any[]) => localFunction.apply(obj, args);
});
}
return obj;
}
private getMutationSetterName(stateKey: string) {
return "set__" + stateKey;
}
}
function mapValues(target: Dictionary, source: Dictionary, mapFunc: (val: S, key: string) => V) {
for (const key of Object.keys(source)) {
target[key] = mapFunc(source[key], key);
}
}
function mapValuesToProperty(
target: Dictionary,
source: Dictionary,
get: (key: string) => any,
set?: (key: string, val: any) => void
) {
for (const key of Object.keys(source)) {
Object.defineProperty(target, key, {
get: () => get(key),
set: set ? (val: string) => set(key, val) : undefined
});
}
}
function getModulePrototypes(module: ModulePrototype): ModulePrototype[] {
const prototypes: ModulePrototype[] = [];
for (let prototype = module; prototype && prototype !== VuexModule; prototype = Object.getPrototypeOf(prototype)) {
prototypes.push(prototype);
}
return prototypes;
}
================================================
FILE: src/module.ts
================================================
import { VuexClassModuleFactory, ModuleOptions, IVuexModule } from "./module-factory";
import { VuexModule } from "./VuexModule";
type VuexModuleClass = new (...args: any[]) => VuexModule;
export function Module(target: T): T;
export function Module(options?: ModuleOptions): ClassDecorator;
export function Module(arg?: ModuleOptions | T): ClassDecorator | T {
if (typeof arg === "function") {
return moduleDecoratorFactory()(arg) as T;
} else {
return moduleDecoratorFactory(arg);
}
}
function moduleDecoratorFactory(moduleOptions?: ModuleOptions) {
return (constructor: TFunction): TFunction => {
const accessor: any = function(...args: any[]) {
const instance = new constructor.prototype.constructor(...args) as IVuexModule;
Object.setPrototypeOf(instance, accessor.prototype);
const factory = new VuexClassModuleFactory(constructor, instance, moduleOptions || {});
factory.registerVuexModule();
return factory.buildAccessor();
};
accessor.prototype = Object.create(constructor.prototype);
accessor.prototype.constructor = accessor;
return accessor;
};
}
================================================
FILE: src/mutations.ts
================================================
import { ModulePrototype } from "./module-factory";
export function Mutation(
target: T,
key: string | symbol,
descriptor: TypedPropertyDescriptor<(arg?: any) => void>
) {
const vuexModule = target.constructor as ModulePrototype;
if (!vuexModule.__mutations) {
vuexModule.__mutations = {};
}
if (descriptor.value) {
vuexModule.__mutations[key as string] = descriptor.value;
}
}
================================================
FILE: test/actions-inheritance.ts
================================================
import { Action, Module, Mutation, VuexModule, RegisterOptions } from "../src";
import Vuex, { Store } from "vuex";
import Vue from "vue";
Vue.use(Vuex);
abstract class ParentModule extends VuexModule {
foo = "init";
get bigFoo() {
return this.foo.toUpperCase();
}
get decoratedFoo() {
return `***${this.foo}***`;
}
@Mutation
updateFoo(value: string) {
this.foo = value;
}
@Action
action1() {
//
}
@Action
action2() {
//
}
@Action
action3() {
//
}
@Action
action4() {
//
}
@Action
action5() {
this.updateFoo(this.foo + "action5");
}
@Action
action6() {
this.updateFoo(this.foo + "polymorphicCallOf");
this.action7();
}
abstract action7(): void;
}
@Module
class Module1 extends ParentModule {
private tag = "child1";
foo: string = "init" + this.tag;
get doubleFoo() {
return this.foo + this.foo;
}
@Mutation
setFooToExample() {
this.foo = "example" + this.tag;
}
@Action
action1() {
this.setFooToExample();
}
@Action
action2() {
this.updateFoo("bar" + this.tag);
}
@Action
action3() {
this.updateFoo(this.doubleFoo);
}
@Action
action4() {
this.updateFoo(this.bigFoo);
}
@Action
action5() {
super.action5();
this.updateFoo(this.foo + this.tag);
}
@Action
action7(): void {
this.updateFoo(this.foo + "action7" + this.tag);
}
}
@Module
class Module2 extends ParentModule {
private tag = "child2";
constructor(options: RegisterOptions) {
super(options);
this.foo = this.foo + this.tag;
}
get tripleFoo() {
return this.foo + this.foo + this.foo;
}
@Mutation
setFooToAnotherExample() {
this.foo = "example" + this.tag;
}
@Action
action1() {
this.setFooToAnotherExample();
}
@Action
action2() {
this.updateFoo("baz" + this.tag);
}
@Action
action3() {
this.updateFoo(this.tripleFoo);
}
@Action
action4() {
this.updateFoo(this.decoratedFoo);
}
@Action
action5() {
super.action5();
this.updateFoo(this.foo + this.tag);
}
@Action
action7(): void {
this.updateFoo(this.foo + "action7" + this.tag);
}
}
describe("actions-inheritance", () => {
let store: Store;
let child1: Module1;
let child2: Module2;
beforeEach(() => {
store = new Vuex.Store({});
child1 = new Module1({ store, name: "child1" });
child2 = new Module2({ store, name: "child2" });
});
test("overriden action has access to mutations", () => {
child1.action1();
child2.action1();
expect(child1.foo).toBe("examplechild1");
expect(child2.foo).toBe("examplechild2");
});
test("overriden action has access to parent mutations", () => {
child1.action2();
child2.action2();
expect(child1.foo).toBe("barchild1");
expect(child2.foo).toBe("bazchild2");
});
test("overriden action has access to getters", () => {
child1.action3();
child2.action3();
expect(child1.foo).toBe("initchild1initchild1");
expect(child2.foo).toBe("initchild2initchild2initchild2");
});
test("overriden action has access to parent getters", () => {
child1.action4();
child2.action4();
expect(child1.foo).toBe("INITCHILD1");
expect(child2.foo).toBe("***initchild2***");
});
test("overriden action has access to parent method implementation", () => {
child1.action5();
child2.action5();
expect(child1.foo).toBe("initchild1action5child1");
expect(child2.foo).toBe("initchild2action5child2");
});
test("parent action access derived action polymorphically", () => {
child1.action6();
child2.action6();
expect(child1.foo).toBe("initchild1polymorphicCallOfaction7child1");
expect(child2.foo).toBe("initchild2polymorphicCallOfaction7child2");
});
test("access action7 directly", () => {
child1.action7();
child2.action7();
expect(child1.foo).toBe("initchild1action7child1");
expect(child2.foo).toBe("initchild2action7child2");
});
});
================================================
FILE: test/actions.ts
================================================
import { Module, Mutation, Action, VuexModule } from "../src";
import Vuex, { Payload, MutationPayload } from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const store = new Vuex.Store({});
@Module
class MyModule extends VuexModule {
documentId = 0;
text = "";
get documentHasText() {
return this.documentId > 10;
}
@Mutation
setDocumentId(id: number) {
this.documentId = id;
}
@Mutation
setText(text: string) {
this.text = text;
}
@Action
async dummyAction(payload: any) {
// to test accessor
}
@Action
async loadText(documentId: number) {
if (this.documentId === 0) {
this.setDocumentId(documentId);
if (this.documentHasText) {
const text = await Promise.resolve("some other text");
this.setText(text);
}
}
}
}
const myModule = new MyModule({ store, name: "myModule" });
interface ActionPayload extends Payload {
payload?: any;
}
describe("actions", () => {
test("accessor dispatches action", async () => {
// subscribeAction missing from vuex typings
const actionObserver = jest.fn((action: ActionPayload) => action);
(store as any).subscribeAction(actionObserver);
await myModule.dummyAction(5);
expect(actionObserver.mock.calls.length).toBe(1);
const mutationPayload = actionObserver.mock.results[0].value as ActionPayload;
expect(mutationPayload.type).toBe("myModule/dummyAction");
expect(mutationPayload.payload).toBe(5);
});
test("'this' matches vuex context", async () => {
const mutationObserver = jest.fn((mutation: MutationPayload) => mutation);
store.subscribe(mutationObserver);
await store.dispatch("myModule/loadText", 11);
expect(mutationObserver.mock.calls.length).toBe(2);
const firstMutation = mutationObserver.mock.results[0].value as MutationPayload;
expect(firstMutation.type).toBe("myModule/setDocumentId");
expect(firstMutation.payload).toBe(11);
const secondMutation = mutationObserver.mock.results[1].value as MutationPayload;
expect(secondMutation.type).toBe("myModule/setText");
expect(secondMutation.payload).toBe("some other text");
});
});
================================================
FILE: test/constructor.ts
================================================
import { Module, VuexModule, RegisterOptions } from "../src";
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const store = new Vuex.Store({});
@Module
class MyModule extends VuexModule {
foo: string;
constructor(foo: string, options: RegisterOptions) {
super(options);
this.foo = foo;
}
}
test("constructor", () => {
const myModule = new MyModule("bar", { store, name: "myModule" });
expect(myModule.foo).toBe("bar");
});
================================================
FILE: test/generate-mutations.ts
================================================
import Vue from "vue";
import Vuex, { MutationPayload } from "vuex";
import { Action, Module, VuexModule } from "../src";
Vue.use(Vuex);
const store = new Vuex.Store({});
@Module({ generateMutationSetters: true })
class MyModule extends VuexModule {
id = 0;
text = "";
@Action
async loadData() {
const { id, text } = await Promise.resolve({ id: 1, text: "some text" });
this.id = id;
this.text = text;
}
}
const myModule = new MyModule({ store, name: "myModule" });
test("generate-mutations", async () => {
const mutationObserver = jest.fn((mutation: MutationPayload) => mutation);
store.subscribe(mutationObserver);
await myModule.loadData();
const firstMutation = mutationObserver.mock.results[0].value as MutationPayload;
expect(firstMutation.type).toBe("myModule/set__id");
expect(firstMutation.payload).toBe(1);
const secondMutation = mutationObserver.mock.results[1].value as MutationPayload;
expect(secondMutation.type).toBe("myModule/set__text");
expect(secondMutation.payload).toBe("some text");
// change state directly using generated mutation
myModule.text = "some other text";
const thirdMutation = mutationObserver.mock.results[2].value as MutationPayload;
expect(thirdMutation.type).toBe("myModule/set__text");
expect(thirdMutation.payload).toBe("some other text");
});
================================================
FILE: test/getters-inheritance.ts
================================================
import { Action, Module, Mutation, VuexModule } from "../src";
import Vuex, { Store } from "vuex";
import Vue from "vue";
Vue.use(Vuex);
class ParentModule extends VuexModule {
foo = "bar";
get bigFoo() {
return this.foo.toUpperCase();
}
get snakeFoo() {
return "__";
}
@Mutation
myMutation(value: string) {
this.foo = value;
}
@Action
myAction() {
if (this.bigFoo === "BAR") {
this.myMutation("ok");
}
if (this.bigFoo === "BAZ") {
this.myMutation("alright");
}
}
}
@Module
class Module1 extends ParentModule {
private tag = "child1";
get snakeFoo() {
return `_${this.foo}_${this.tag}_`;
}
}
@Module
class Module2 extends ParentModule {
private tag = "child2";
foo = "baz";
get snakeFoo() {
return `_${this.foo}_${this.tag}_`;
}
}
describe("getters-inheritance", () => {
let store: Store;
let child1: Module1;
let child2: Module2;
beforeEach(() => {
store = new Vuex.Store({});
child1 = new Module1({ store, name: "child1" });
child2 = new Module2({ store, name: "child2" });
});
test("allows the use of getters from an inheriting class", () => {
expect(child1.bigFoo).toBe("BAR");
expect(child1.bigFoo).toBe(store.getters["child1/bigFoo"]);
expect(child2.bigFoo).toBe("BAZ");
expect(child2.bigFoo).toBe(store.getters["child2/bigFoo"]);
});
test("allows the use of getters in inherited class", () => {
child1.myAction();
child2.myAction();
expect(child1.bigFoo).toBe("OK");
expect(child1.bigFoo).toBe(store.getters["child1/bigFoo"]);
expect(child2.bigFoo).toBe("ALRIGHT");
expect(child2.bigFoo).toBe(store.getters["child2/bigFoo"]);
});
test("overriden getters behave as expected", () => {
expect(child1.snakeFoo).toBe("_bar_child1_");
expect(child1.snakeFoo).toBe(store.getters["child1/snakeFoo"]);
expect(child2.snakeFoo).toBe("_baz_child2_");
expect(child2.snakeFoo).toBe(store.getters["child2/snakeFoo"]);
});
});
================================================
FILE: test/getters.ts
================================================
import { Module, VuexModule } from "../src";
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const store = new Vuex.Store({});
@Module
class MyModule extends VuexModule {
foo = {
text: "some text"
};
get textTransforms() {
return {
original: this.foo.text,
upperCase: this.foo.text.toUpperCase()
};
}
}
const myModule = new MyModule({ store, name: "myModule" });
test("getters", () => {
expect(myModule.textTransforms).toBe(store.getters["myModule/textTransforms"]);
expect(myModule.textTransforms.upperCase).toBe("SOME TEXT");
});
================================================
FILE: test/instanceof.ts
================================================
import { Module, VuexModule } from "../src";
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const store = new Vuex.Store({});
class OriginalModule extends VuexModule {
foo = {
text: "some text"
};
bar = 1;
}
/** Manually apply decorator, to have access to initial class definition */
const MyModule = Module(OriginalModule);
const myModule = new MyModule({ store, name: "myModule" });
test("instance of", () => {
expect(myModule instanceof OriginalModule).toBe(true);
expect(myModule instanceof MyModule).toBe(true);
expect(myModule instanceof VuexModule).toBe(true);
});
================================================
FILE: test/local-functions-inheritance.ts
================================================
import Vue from "vue";
import Vuex, { Store } from "vuex";
import { Action, Module, Mutation, VuexModule } from "../src";
Vue.use(Vuex);
abstract class Parent extends VuexModule {
canTransform = true;
text = "parent text";
get upperCaseText() {
return this.text.toUpperCase();
}
get noSpaceText() {
return this.snakeText();
}
private snakeText() {
// 'this' has access to state & getters
return this.canTransform ? this.upperCaseText.replace(/ /g, "_") : "";
}
@Mutation
setText(text: string) {
this.localSetText(text);
}
@Mutation
clearText() {
this.text = "";
}
protected abstract localSetText(text: string): void;
protected localLoadText() {
this.setText("parent: yet another text");
}
}
@Module
class Module1 extends Parent {
private tag = "child1";
canTransform = true;
text = `${this.tag} text`;
get upperCaseText() {
return this.text.toUpperCase();
}
protected localSetText(text: string) {
// 'this' has state
this.text = `${text} ${this.tag}`;
}
@Action
async loadText() {
this.localLoadText();
}
protected localLoadText() {
// 'this' has getters & mutations
if (!this.upperCaseText) {
this.setText(`${this.tag.toUpperCase()}: yet another text`);
}
}
}
@Module
class Module2 extends Parent {
private tag = "child2";
canTransform = true;
text = `${this.tag} text`;
get upperCaseText() {
return this.text.toUpperCase();
}
get noSpaceText() {
return this.dashText();
}
private dashText() {
// 'this' has access to state & getters
return this.canTransform ? this.upperCaseText.replace(/ /g, "--") : "";
}
protected localSetText(text: string) {
// 'this' has state
this.text = `***${text}*** ${this.tag}`;
}
@Action
async loadText() {
this.localLoadText();
}
protected localLoadText() {
// 'this' has getters & mutations
if (!this.upperCaseText) {
this.setText(`${this.tag}: yet another text`);
}
}
}
describe("local-functions", () => {
let child1: Module1;
let child2: Module2;
let store: Store;
beforeEach(() => {
store = new Vuex.Store({});
child1 = new Module1({ store, name: "myModule1" });
child2 = new Module2({ store, name: "myModule2" });
});
test("from getter", () => {
expect(child1.noSpaceText).toBe("CHILD1_TEXT");
expect(child2.noSpaceText).toBe("CHILD2--TEXT");
});
test("from mutation", () => {
child1.setText("some other text");
child2.setText("some other text");
expect(child1.text).toBe("some other text child1");
expect(child2.text).toBe("***some other text*** child2");
});
test("from action", async () => {
child1.clearText();
child2.clearText();
await child1.loadText();
await child2.loadText();
expect(child1.text).toBe("CHILD1: yet another text child1");
expect(child2.text).toBe("***child2: yet another text*** child2");
});
});
================================================
FILE: test/local-functions.ts
================================================
import Vue from "vue";
import Vuex from "vuex";
import { Action, Module, Mutation, VuexModule } from "../src";
Vue.use(Vuex);
const store = new Vuex.Store({});
@Module
class MyModule extends VuexModule {
canTransform = true;
text = "some text";
get upperCaseText() {
return this.text.toUpperCase();
}
get pascalText() {
return this.transformText();
}
private transformText() {
// 'this' has access to state & getters
return this.canTransform ? this.upperCaseText.replace(/ /g, "_") : "";
}
@Mutation
setText(text: string) {
this.localSetText(text);
}
private localSetText(text: string) {
// 'this' has state
this.text = text;
}
@Action
async loadText() {
this.localLoadText();
}
private localLoadText() {
// 'this' has getters & mutations
if (!this.upperCaseText) {
this.setText("yet another text");
}
}
}
const myModule = new MyModule({ store, name: "myModule" });
describe("local-functions", () => {
test("from getter", () => {
expect(myModule.pascalText).toBe("SOME_TEXT");
});
test("from mutation", () => {
myModule.setText("some other text");
expect(myModule.text).toBe("some other text");
});
test("from action", async () => {
myModule.setText("");
await myModule.loadText();
expect(myModule.text).toBe("yet another text");
});
});
================================================
FILE: test/module-reference.ts
================================================
import { Module, VuexModule, RegisterOptions } from "../src";
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const store = new Vuex.Store({});
@Module
class MyModule extends VuexModule {
foo = "bar";
}
@Module
class OtherModule extends VuexModule {
private myModule: MyModule;
get moduleRef() {
return this.myModule;
}
constructor(myModule: MyModule, options: RegisterOptions) {
super(options);
this.myModule = myModule;
}
}
test("module references", () => {
const myModule = new MyModule({ store, name: "myModule" });
const otherModule = new OtherModule(myModule, { store, name: "otherModule" });
expect(store.state.otherModule.myModule).toBeUndefined();
expect(otherModule.moduleRef).toBe(myModule);
});
================================================
FILE: test/mutations-inheritance.ts
================================================
import { Module, Mutation, VuexModule } from "../src";
import Vuex, { Store } from "vuex";
import Vue from "vue";
Vue.use(Vuex);
class ParentModule extends VuexModule {
foo = "init";
@Mutation
mutation1(value: string) {
//
}
@Mutation
mutation2(value: string) {
//
}
@Mutation
mutation3(value: string) {
this.foo = value;
}
}
@Module
class Module1 extends ParentModule {
private tag = "child1";
baz = "init" + this.tag;
@Mutation
mutation1(value: string) {
this.baz = value + this.tag;
}
@Mutation
mutation2(value: string) {
this.foo = value + this.tag;
}
@Mutation
mutation3(value: string) {
super.mutation3(value);
this.foo = this.foo + this.tag;
}
}
@Module
class Module2 extends ParentModule {
private tag = "child2";
bar = "init" + this.tag;
baz = "init" + this.tag;
@Mutation
mutation1(value: string) {
this.bar = value + this.tag;
}
@Mutation
mutation2(value: string) {
this.baz = value + this.tag;
}
@Mutation
mutation3(value: string) {
super.mutation3(value);
this.foo = this.foo + this.tag;
}
}
describe("mutations-inheritance", () => {
let store: Store;
let parent: ParentModule;
let child1: Module1;
let child2: Module2;
beforeEach(() => {
store = new Vuex.Store({});
parent = new ParentModule({ store, name: "parentModule" });
child1 = new Module1({ store, name: "myModule1" });
child2 = new Module2({ store, name: "myModule2" });
});
test("overriden mutation can modify state", () => {
parent.mutation1("_");
child1.mutation1("bar1");
child2.mutation1("bar2");
expect(parent.foo).toBe("init");
expect(child1.baz).toBe("bar1child1");
expect(child2.bar).toBe("bar2child2");
expect(child2.baz).toBe("initchild2");
});
test("overriden mutation can modify parent state", () => {
parent.mutation2("_");
child1.mutation2("bar");
child2.mutation2("baz");
expect(parent.foo).toBe("init");
expect(child1.foo).toBe("barchild1");
expect(child2.bar).toBe("initchild2");
expect(child2.baz).toBe("bazchild2");
});
test("overriden mutation has access to parent method implementation", () => {
parent.mutation3("foo_");
child1.mutation3("foo1");
child2.mutation3("foo2");
expect(parent.foo).toBe("foo_");
expect(child1.foo).toBe("foo1child1");
expect(child1.baz).toBe("initchild1");
expect(child2.foo).toBe("foo2child2");
expect(child2.bar).toBe("initchild2");
expect(child2.baz).toBe("initchild2");
});
});
================================================
FILE: test/mutations.ts
================================================
import { Module, Mutation, VuexModule } from "../src";
import Vuex, { MutationPayload } from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const store = new Vuex.Store({});
@Module
class MyModule extends VuexModule {
shouldUpdate = true;
text = "";
@Mutation
setText(text: string) {
if (this.shouldUpdate) {
this.text = text;
}
}
}
const myModule = new MyModule({ store, name: "myModule" });
describe("mutations", () => {
test("accessor calls commit", () => {
const mutationObserver = jest.fn((mutation: MutationPayload) => mutation);
store.subscribe(mutationObserver);
myModule.setText("some text");
expect(mutationObserver.mock.calls.length).toBe(1);
const mutationPayload = mutationObserver.mock.results[0].value as MutationPayload;
expect(mutationPayload.type).toBe("myModule/setText");
expect(mutationPayload.payload).toBe("some text");
});
test("updates store", () => {
store.commit("myModule/setText", "some other text");
expect(store.state.myModule.text).toBe("some other text");
});
});
================================================
FILE: test/state.ts
================================================
import { Module, VuexModule } from "../src";
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const store = new Vuex.Store({});
@Module
class MyModule extends VuexModule {
foo = {
text: "some text"
};
bar = 1;
square = (num: number) => num * num;
}
const myModule = new MyModule({ store, name: "myModule" });
test("state", () => {
expect(myModule.foo).toBe(store.state.myModule.foo);
expect(myModule.foo.text).toBe("some text");
expect(myModule.bar).toBe(store.state.myModule.bar);
expect(myModule.bar).toBe(1);
expect(myModule.square).toBe(store.state.myModule.square);
expect(myModule.square(2)).toBe(4);
});
================================================
FILE: test/tsconfig.json
================================================
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "",
"experimentalDecorators": true,
"declaration": false
},
"include": [
"./**/*.ts"
]
}
================================================
FILE: test/watch.ts
================================================
import { Module, Mutation, Action, VuexModule } from "../src";
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const store = new Vuex.Store({});
@Module
class MyModule extends VuexModule {
text = "";
get getText() {
return this.text;
}
@Mutation
setText(text: string) {
this.text = text;
}
@Action
async changeText(text: string) {
this.setText(text);
}
}
const myModule = new MyModule({ store, name: "myModule" });
describe("watch", () => {
test("watch callback is called", async () => {
const watchCallback = jest.fn((newValue: string, oldValue: string) => undefined);
myModule.setText("bar");
myModule.$watch(theModule => theModule.getText, watchCallback);
await myModule.changeText("foo");
expect(watchCallback.mock.calls.length).toBe(1);
expect(watchCallback.mock.calls[0].length).toBe(2);
expect(watchCallback.mock.calls[0][0]).toBe("foo");
expect(watchCallback.mock.calls[0][1]).toBe("bar");
});
test("watch for state changes as well", async () => {
const watchCallback = jest.fn((newValue: string, oldValue: string) => undefined);
myModule.setText("bar");
myModule.$watch(theModule => theModule.text, watchCallback);
await myModule.changeText("foo");
expect(watchCallback.mock.calls.length).toBe(1);
expect(watchCallback.mock.calls[0].length).toBe(2);
expect(watchCallback.mock.calls[0][0]).toBe("foo");
expect(watchCallback.mock.calls[0][1]).toBe("bar");
});
test("watch should return unwatch func", async () => {
const watchCallback = jest.fn((newValue: string, oldValue: string) => undefined);
myModule.setText("bar");
const unwatch = myModule.$watch(theModule => theModule.text, watchCallback);
await myModule.changeText("foo1");
expect(watchCallback.mock.calls.length).toBe(1);
unwatch();
await myModule.changeText("foo2");
expect(watchCallback.mock.calls.length).toBe(1);
});
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"lib": [
"dom",
"es2015"
],
"declaration": true,
"outDir": "./lib",
"strict": true,
"experimentalDecorators": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}
================================================
FILE: tslint.json
================================================
{
"extends": [
"tslint:recommended",
"tslint-config-prettier"
],
"rulesDirectory": ["tslint-plugin-prettier"],
"rules": {
"prettier": [true, { "printWidth": 120 }],
"no-namespace": false,
"max-line-length": false,
"interface-name": false,
"arrow-parens": [false],
"object-literal-sort-keys": false,
"ordered-imports": [
false
],
"member-access": [
true, "no-public"
],
"max-classes-per-file": false,
"trailing-comma": [
false
],
"interface-over-type-literal": false,
"no-console": [false],
"one-line": false,
"curly": false,
"no-empty": [true, "allow-empty-catch", "allow-empty-functions"],
"member-ordering": false,
"no-unused-expression": false,
"only-arrow-functions": false,
"ban-types": [true, "Function"],
"variable-name": false
},
"defaultSeverity": "warning"
}