GithubHelp home page GithubHelp logo

ktsn / vuex-smart-module Goto Github PK

View Code? Open in Web Editor NEW
382.0 6.0 19.0 2.1 MB

Type safe Vuex module with powerful module features

License: MIT License

JavaScript 3.85% TypeScript 96.15%
vuejs vuex type-safety typescript

vuex-smart-module's Introduction

vuex-smart-module

Type safe Vuex module with powerful module features. The basic API idea is brought from Sinai.

Features

  • Completely type safe when used with TypeScript without redundancy.
  • Provide a smart way to use modules.
  • Canonical Vuex-like API as possible.

Installation

$ npm install vuex-smart-module

Usage

All examples are written in TypeScript

You create a module with class syntax:

// store/modules/foo.ts

// Import base classes
import { Getters, Mutations, Actions, Module } from 'vuex-smart-module'

// State
class FooState {
  count = 1
}

// Getters
// Extend 'Getters' class with 'FooState' type
class FooGetters extends Getters<FooState> {
  // You can declare both getter properties or methods
  get double() {
    // Getters instance has 'state' property
    return this.state.count * 2
  }

  get triple() {
    // When you want to use another getter, there is `getters` property
    return this.getters.double + this.state.count
  }
}

// Mutations
// Extend 'Mutations' class with 'FooState' type
class FooMutations extends Mutations<FooState> {
  increment(payload: number) {
    // Mutations instance has 'state' property.
    // You update 'this.state' by mutating it.
    this.state.count += payload
  }
}

// Actions
// Extend 'Actions' class with other module asset types
// Note that you need to specify self action type (FooActions) as a type parameter explicitly
class FooActions extends Actions<
  FooState,
  FooGetters,
  FooMutations,
  FooActions
> {
  incrementAsync(payload: { amount: number; interval: number }) {
    // Actions instance has 'state', 'getters', 'commit' and 'dispatch' properties

    return new Promise(resolve => {
      setTimeout(() => {
        this.commit('increment', payload.amount)
        resolve()
      }, payload.interval)
    })
  }
}

// Create a module with module asset classes
export const foo = new Module({
  state: FooState,
  getters: FooGetters,
  mutations: FooMutations,
  actions: FooActions
})

Then, create Vuex store instance by using createStore function from vuex-smart-module:

// store/index.ts

import Vue from 'vue'
import * as Vuex from 'vuex'
import { createStore, Module } from 'vuex-smart-module'
import { foo } from './modules/foo'

Vue.use(Vuex)

// The 1st argument is root module.
// Vuex store options should be passed to the 2nd argument.
export const store = createStore(
  // Root module
  foo,

  // Vuex store options
  {
    strict: process.env.NODE_ENV !== 'production'
  }
)

The created store is a traditional instance of Vuex store - you can use it in the same manner.

// main.ts

import Vue from 'vue'
import { store } from './store'
import App from './App.vue'

new Vue({
  el: '#app',
  store,
  render: h => h(App)
})

Nested Modules

You can create a nested module as same as Vuex by passing a module object to another module's modules option.

import { Getters, Module, createStore } from 'vuex-smart-module'

class NestedState {
  value = 'hello'
}

class NestedGetters extends Getters<NestedState> {
  greeting(name: string): string {
    return this.state.value + ', ' + name
  }
}

const nested = new Module({
  state: NestedState,
  getters: NestedGetters
})

const root = new Module({
  modules: {
    nested
  }
})

const store = createStore(root)

console.log(store.state.nested.value) // -> hello
console.log(store.getters['nested/greeting']('John')) // -> hello, John

Nested modules will be namespaced module by default. If you do not want a module to be a namespaced, pass the namespaced: false option to the module's constructor options.

import { Getters, Module, createStore } from 'vuex-smart-module'

class NestedState {
  value = 'hello'
}

class NestedGetters extends Getters<NestedState> {
  greeting(name: string): string {
    return this.state.value + ', ' + name
  }
}

const nested = new Module({
  // nested module will not be namespaced
  namespaced: false

  state: NestedState,
  getters: NestedGetters
})

const root = new Module({
  modules: {
    nested
  }
})

const store = createStore(root)

console.log(store.state.nested.value) // -> hello
console.log(store.getters.greeting('John')) // -> hello, John

Module Lifecycle and Dependencies

Getters and actions class can have a special method $init which will be called after the module is initialized in a store. The $init hook receives the store instance as the 1st argument. You can pick some external dependencies from it. The following is an example for Nuxt + Axios Module.

import { Store } from 'vuex'
import { Actions } from 'vuex-smart-module'

class FooActions extends Actions {
  // Declare dependency type
  store: Store<any>

  // Called after the module is initialized
  $init(store: Store<any>): void {
    // Retain store instance for later
    this.store = store
  }

  async fetch(): Promise<void> {
    console.log(await this.store.$axios.$get('...'))
  }
}

There are no rootState, rootGetters and root options on dispatch, commit because they are too difficult to type and the code has implicit dependencies to other modules. In case of you want to use another module in some module, you can create a module context.

import { Store } from 'vuex'
import { Getters, Actions, Module, Context } from 'vuex-smart-module'

// Foo module
class FooState {
  value = 'hello'
}

const foo = new Module({
  state: FooState
})

// Bar module (using foo module in getters and actions)
class BarGetters extends Getters {
  // Declare context type
  foo: Context<typeof foo>

  // Called after the module is initialized
  $init(store: Store<any>): void {
    // Create and retain foo module context
    this.foo = foo.context(store)
  }

  get excited(): string {
    return this.foo.state.value + '!' // -> hello!
  }
}

class BarActions extends Actions {
  // Declare context type
  foo: Context<typeof foo>

  // Called after the module is initialized
  $init(store: Store<any>): void {
    // Create and retain foo module context
    this.foo = foo.context(store)
  }

  print(): void {
    console.log(this.foo.state.value) // -> hello
  }
}

const bar = new Module({
  getters: BarGetters,
  actions: BarActions
})

// Make sure to have all modules in the store
const root = new Module({
  modules: {
    foo,
    bar
  }
})

const store = createStore(root)

Nested Module Context

When there are nested modules in your module, you can access them through a module context.

Let's say you have three modules: counter, todo and root where the root module has former two modules as nested modules:

import { Module, createStore } from 'vuex-smart-module'

// Counter module
const counter = new Module({
  // ...
})

// Todo module
const todo = new Module({
  // ...
})

// Root module
const root = new Module({
  modules: {
    counter,
    todo
  }
})

export const store = createStore(root)

You can access counter and todo contexts through the root context by using modules property.

import { root, store } from './store'

// Get root context
const ctx = root.context(store)

// You can access counter and todo context through `modules` as well
const counterCtx = ctx.modules.counter
const todoCtx = ctx.modules.todo

counterCtx.dispatch('increment')
todoCtx.dispatch('fetchTodos')

Register Module Dynamically

You can use registerModule to register a module and unregisterModule to unregister it.

import { registerModule, unregisterModule } from 'vuex-smart-module'
import { store } from './store'
import { foo } from './store/modules/foo'

// register module
registerModule(
  store, // store instance
  ['foo'], // module path. can be string or array of string
  'foo/', // namespace string which will be when put into the store
  foo, // module instance

  // module options as same as vuex registerModule
  {
    preserveState: true
  }
)

// unregister module
unregisterModule(
  store, // store instance
  foo // module instance which you want to unregister
)

Note that the 3rd argument of registerModule, which is the namespace string, must match with the actual namespace that the store resolves. If you pass the wrong namespace to it, component mappers and context api would not work correctly.

Component Mapper

You can generate mapXXX helpers, which are the same interface as Vuex ones, for each associated module by using the createMapper function. The mapped computed properties and methods are strictly typed. So you will not have some typo or pass wrong payloads to them.

// @/store/modules/foo
import { Module, createMapper } from 'vuex-smart-module'

// Create module
export const foo = new Module({
  // ...
})

// Create mapper
export const fooMapper = createMapper(foo)
import Vue from 'vue'

// Import foo mapper
import { fooMapper } from '@/store/modules/foo'

export default Vue.extend({
  computed: fooMapper.mapGetters(['double']),

  methods: fooMapper.mapActions({
    incAsync: 'incrementAsync'
  }),

  created() {
    console.log(this.double)
    this.incAsync(undefined)
  }
})

Composable Function

If you prefer composition api for binding a store module to a component, you can create a composable function by using createComposable.

// @/store/modules/foo
import { Module, createComposable } from 'vuex-smart-module'

// Create module
export const foo = new Module({
  // ...
})

// Create composable function
export const useFoo = createComposable(foo)
import { defineComponent } from '@vue/composition-api'

// Import useFoo
import { useFoo } from '@/store/modules/foo'

export default defineComponent({
  setup() {
    // Get Foo module's context
    const foo = useFoo()

    console.log(foo.getters.double)
    foo.dispatch('incrementAsync')
  }
})

Method Style Access for Actions and Mutations

this in an action and a module context have actions and mutations properties. They contains module actions and mutations in method form. You can use them instead of dispatch or commit if you prefer method call style over event emitter style.

The method style has several advantages: you can use Go to definition for your actions and mutations and it prints simple and easier to understand errors if you pass a wrong payload type, for example.

Example usage in an action:

import { Actions } from 'vuex-smart-module'

class FooActions extends Actions<FooState, FooGetters, FooMutations, FooActions> {
  increment(amount: number)
    // Call `increment` mutation
    this.mutations.increment(payload)
  }
}

Example usage via a context:

import Vue from 'vue'

// Import foo module
import { foo } from '@/store/modules/foo'

export default Vue.extend({
  mounted() {
    const ctx = foo.context(this.$store)

    // Call `increment` action
    ctx.actions.increment(1)
  }
})

Using in Nuxt's Modules Mode

You can use Module#getStoreOptions() method to use vuex-smart-module in Nuxt's module mode.

When you have a counter module like the below:

// store/counter.ts
import { Getters, Actions, Mutations, Module } from 'vuex-smart-module'

export class CounterState {
  count = 0
}

export class CounterGetters extends Getters<CounterState> {
  get double() {
    return this.state.count * 2
  }
}

export class CounterMutations extends Mutations<CounterState> {
  inc() {
    this.state.count++
  }
}

export class CounterActions extends Actions<CounterState, CounterGetters, CounterMutations> {
  inc() {
    this.commit('inc')
  }
}

export default new Module({
  state: CounterState,
  getters: CounterGetters,
  mutations: CounterMutations,
  actions: CounterActions
})

Construct a vuex-smart-module root module and export the store options acquired with getStoreOptions in store/index.ts. Note that you have to register all nested modules through the root module:

// store/index.ts
import { Module } from 'vuex-smart-module'
import counter from './counter'

const root = new Module({
  modules: {
    counter
  }
})

export const {
  state,
  getters,
  mutations,
  actions,
  modules,
  plugins
} = root.getStoreOptions()

If you want to extend a store option, you can manually modify it:

// store/index.ts
const options = root.getStoreOptions()

export const {
  state,
  getters,
  mutations,
  actions,
  modules
} = options

// Add an extra plugin
export const plugins = options.plugins.concat([otherPlugin])

Hot Module Replacement

To utilize hot module replacement for the store created with vuex-smart-module, we provide hotUpdate function.

The below is an example how to use hotUpdate function:

import { createStore, hotUpdate } from 'vuex-smart-module'
import root from './root'

export const store = createStore(root)

if (module.hot) {
  // accept actions and mutations as hot modules
  module.hot.accept(['./root'], () => {
    // require the updated modules
    // have to add .default here
    const newRoot = require('./root').default

    // swap in the new root module by using `hotUpdate` provided from vuex-smart-module.
    hotUpdate(store, newRoot)
  })
}

Note that you cannot use hotUpdate under Vuex store instance. Use hotUpdate function imported from vuex-smart-module.

Testing

Unit testing getters, mutations and actions

vuex-smart-module provides the inject helper function which allows you to inject mock dependencies into getters, mutations and actions instances. You can inject any properties for test:

import { inject } from 'vuex-smart-module'
import { FooGetters, FooActions } from '@/store/modules/foo'

it('returns doubled value', () => {
  // Inject mock state into getters
  const getters = inject(FooGetters, {
    state: {
      count: 5
    }
  })

  // Test double getter
  expect(getters.double).toBe(10)
})

it('increments asynchronously', async () => {
  // Inject mock commit method
  const commit = jest.fn()
  const actions = inject(FooActions, {
    commit
  })

  await actions.incrementAsync({
    amount: 3
    interval: 1
  })

  // Check mock commit method is called
  expect(commit).toHaveBeenCalledWith('increment', 3)
})

Mocking modules to test components

When you want to mock some module assets, you can directly inject a mock constructor into the module options. For example, you will test the following component which is using the counter module:

<template>
  <button @click="increment">Increment</button>
</template>

<script lang="ts">
import Vue from 'vue'

// use counter Mapper
import { counterMapper } from '@/store/modules/counter'

export default Vue.extend({
  methods: counterMapper.mapMutations(['increment'])
})
</script>

In the spec file, mock the mutations option in the counter module. The below is a Jest example but the essential idea holds true for many test frameworks:

import * as Vuex from 'vuex'
import { shallowMount, createLocalVue } from '@vue/test-utils'
import { createStore } from 'vuex-smart-module'

// component which we want to test
import Counter from '@/components/Counter.vue'

// counter module which we want to mock
import counter, { CounterMutations } from '@/store/modules/counter'

const localVue = createLocalVue()
localVue.use(Vuex)

// make sure that you clean mocked object after each test case
const originalMutations = counter.options.mutations
afterEach(() => {
  counter.options.mutations = originalMutations
})

it('calls increment mutation', () => {
  // create spy
  const spy = jest.fn()

  // create mock mutation
  class MockMutations extends CounterMutations {
    // override increment method for mock
    increment() {
      spy()
    }
  }

  // inject mock
  counter.options.mutations = MockMutations

  // create mock store
  const store = createStore(counter)

  // emulate click event
  shallowMount(Counter, { store, localVue }).trigger('click')

  // check the mock function was called
  expect(spy).toHaveBeenCalled()
})

Mocking nested modules and dependencies

Using dependencies and nested module contexts in Actions requires to mock them in tests.

So you test the following Actions class that has been constructed as described in the section above:

import { Store } from 'vuex'
import { Actions } from 'vuex-smart-module'

class FooActions extends Actions {
  // Declare dependency type
  store!: Store<FooState>
  bar!: Context<typeof bar>

  // Called after the module is initialized
  $init(store: Store<FooState>): void {
    // Retain store instance for later
    this.store = store
    this.bar = bar.context(store)
  }

  async fetch(): Promise<void> {
    console.log(await this.store.$axios.$get('...'))
    this.bar.dispatch(...)
  }
}

Then the Jest spec file would be written as:

import { inject } from 'vuex-smart-module'
import { FooActions } from '@/store/modules/foo'

describe('FooActions', () => {
  it('calls the dependency and dispatches the remote action', async () => {
    const axiosGet = jest.fn()
    const barDispatch = jest.fn()

    const actions = inject(FooActions, {
      store: {
        $axios: {
          $get: axiosGet
        }
      },

      bar: {
        dispatch: barDispatch
      }
    })

    await actions.fetch()

    expect(axiosGet).toHaveBeenCalledWith(...)
    expect(barDispatch).toHaveBeenCalledWith(...)
  })
})

License

MIT

vuex-smart-module's People

Contributors

1000ch avatar dependabot-preview[bot] avatar dependabot[bot] avatar ilajosmanov avatar ktsn avatar lucaswerkmeister avatar micgro42 avatar odanado avatar pipopotamasu avatar seminioni avatar vdkrasny avatar wiese avatar wonderful-panda avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

vuex-smart-module's Issues

Problem with migrating an existing project

Hello, @ktsn, how can i use modules in root and in options in same time? like this

const root = new Module({
  modules: {
    // typed modules
  },
})

export const store = createStore(
  root,
  {
    modules: {
        // non typed modules
      }
  }
)

spread operator removes modules from rootModuleOptions
probably problem in creating new Store

const store: Store<any> = new Store({

can do the same as with plugins? and i can create a PR if you agree

new Store({
    ...rootModuleOptions,
    ...options,
    modules: {
       ...rootModuleOptions.modules,
       ...options.modules,
    },
    plugins: [injectStore].concat(options.plugins || []),
  })

this would greatly simplify the project migration

Type safety availability

Regarding this adapted code from the README example:

import { Getters, Module, createStore } from 'vuex-smart-module'

class NestedState {
  value = 'hello'
}

class NestedGetters extends Getters<NestedState> {
  greeting(name: string): string {
    return this.state.value + ', ' + name
  }
}

const nested = new Module({
  state: NestedState,
  getters: NestedGetters
})

const root = new Module({
  modules: {
    nested
  }
})

const store = createStore(root)

// [1]
console.log(store.state.nested.value) // -> hello
console.log(store.getters['nested/greeting']('John')) // -> hello, John

// [2]
const greeting = nested.mapGetters(['greeting']).greeting()
greeting(true) // ts error: Argument of type 'true' is not assignable to parameter of type 'string'

// [3]
root.mapGetters(['nested/greeting']) // ts error: Type 'string' is not assignable to type '"$init"'
  1. Do we have type safety in these 2 lines? It doesn't seem like that to me. I can write console.log(store.state.nested.fakeValue), store.getters['nested/fakeGreeting']('John') with no errors.
  2. Type checking works. โœ”๏ธ
  3. I can't find how to access from root a getter belonging to the nested module.

Thanks

Support auto mutation

Hi guys!

When I used another approach - vuex-module-decorators it gave me cool way for auto generation Mutations in cases when you need map data on State, which got Action, without any changing. In some cases it will be very usefull because you don't need create extra Mutations.

And it will be cool see something like this in your approach! ๐Ÿ˜Š
Now in my project I created little patch of class Module for this purpose:

import { 
    Module as SmartModule,
    Getters,
    Mutations,
    Actions,
} from 'vuex-smart-module';
import { Class, } from 'vuex-smart-module/lib/utils';

class EmptyMutations<S> extends Mutations<S> {}

export default class Module<S, G extends Getters<S>, M extends Mutations<S>, A extends Actions<S, G, M> > extends SmartModule<S, G, M, A> {

    create(path: string[], namespace: string) {
        const actions = this.options.actions?.prototype;
        actions && Object.getOwnPropertyNames(actions)
            .filter(name => name !== 'constructor')
            .forEach(method => {
                const oldAction = actions[method];
                actions[method] = async function(payload?: any) {
                    const actionPayload = await oldAction.call(this, payload);
                    this.commit(method, actionPayload);
                }
                
                this.options.mutations = this.options.mutations || EmptyMutations as Class<M>;
                this.options.mutations.prototype[method] = function(payload?: any) {
                    payload && Object.keys(payload).forEach(key => {
                        if (this.state.hasOwnProperty(key)) {
                            this.state[key] = payload[key] || null;
                        } else {
                            throw Error(`Error during mutation: param with name ${key} doesn't contains in state`);
                        }
                    });
                }
            });

        return super['create'](path, namespace);
    }

And now I can create such Action:

async test() {
    // API call
    return {
        // mapping on state fields
    };
}

Get type error when Mutations is without arguments

Maybe, payload of Commit should replace to payload?.

code

import { Getters, Mutations, Actions } from "vuex-smart-module";

class FooState {
  count = 1;
}

class FooGetters extends Getters<FooState> {}

class FooMutations extends Mutations<FooState> {
  increment() {
    this.state.count += 1;
  }
}

class FooActions extends Actions<
  FooState,
  FooGetters,
  FooMutations,
  FooActions
> {
  incrementAsync(payload: { interval: number }) {
    return new Promise(_ => {
      setTimeout(() => {
        this.commit("inc");
      }, payload.interval);
    });
  }
}

command

$ yarn tsc --noEmit
yarn run v1.9.4
$ /Users/user/source/javascript/ghkw-web/node_modules/.bin/tsc --noEmit
src/poyo.ts:24:21 - error TS2345: Argument of type '"inc"' is not assignable to parameter of type 'undefined & { type: "increment"; }'.
  Type '"inc"' is not assignable to type 'undefined'.

24         this.commit("inc");
                       ~~~~~

Does not work in SSR

When I wanna call action in serverPrefetch - it's don't work. Do You have solution?

Action types getting lost

Hello there,

following your recommendation to use this library over sinai I've recently been experimenting with this library.

However, I'm running into the issue that generic types & action parameters are getting lost. This is especially problematic with promises, since almost all my actions either accept parameters or sometimes even return data (e.g IDs of the created datasets)

To illustrate: This is the action "loadRoom" in the class of my "Room" store:

image

And when I use it:

gh_missingtypes

How can I fix this? / What did I do wrong?

Thanks for your time!

Mistake in arguments

Look, I have mutation without parameters. But commit in action I see next error: I must pass arguments ( {} or undefined )
TypeScript: 3.4.3
Screenshot from 2019-04-25 18-53-38
Screenshot from 2019-04-25 19-00-17

Hot Module Replacement (HMR)

Is it possible to implement HMR with vuex-smart-module?
Currently I'm trying to do something like that:

import { myModule} from '@store/my-module/module';

const store = createStore(new Module({
    modules: { myModule },
  }),
);

if (module['hot']) {
  module['hot'].accept(['./my-module/module'], (path) => {
    console.log('Hot updating the store, ', path);
    const myModule = require('./my-module/module').myModule;
    store.hotUpdate({ modules: { myModule } })
  })
}

But it does not look right and I get following error from one of my components which is using myModule.context(this.$store); after I change something in myModule to trigger HMR:

[Vue warn]: Error in mounted hook (Promise/async): "Error: [vuex-smart-module] The module need to be registered a store before using Module#context or createMapper"

Nuxt Module is not defined in vuex store $init method

Trying to get it work by following example:

axios: AxiosInstance

// Called after the module is initialized
$init(store: Store<any>): void {
  // Retain axios instance for later
  this.axios = store.$axios
  console.log(this.axios) // <--- returns undefined
}

After i force push it to the event loop, for better understanding a flow

setTimeout(() => {
  console.log(store.$axios) // <--- returns an axios instance
}, 0);

Then i dig it up to the ./nuxt/index.js and it seems to me like a very big space between vuex store and nuxt-modules registration

In my project: vuex - line 51, nuxt-modules - 158

By now, i'm using following solution of linking store right inside vuex-module:

store!: Store<any>

$init (store: Store<any>): void {
  this.store = store
}

async anAsyncAction () {
  await this.store.$axios.get('/api/item')
}

It works, but not that perfect, for example: proxying work, but some configuration like a prefix doesn't

vuex-smart-module or vuex-class

Could you explain what better to use currently for typification of vuex, this library or vuex-class? As I see all developers activity here but the community used mainly vuex-class.

Type problem when using mapMutations

I found strange behavior (maybe a bug?) when using mapMutations. Here is repo to reproduce: https://github.com/NikitaKA/vuex-smart-module-bug

What happening? When adding mutation to mapMutations their types become unioned:

store.ts

/* โ€ฆ */
class TestMutations extends Mutations<TestState> {
    setTest(value: boolean) {
        this.state.test = value;
    }

    setFoo(value: string) {
        this.state.foo = value;
    }
}
/* โ€ฆ */

app.vue

const Super = Vue.extend({
  methods: TestStore.mapMutations(['setTest', 'setFoo'])
});

@Component
export default class App extends Super {
  mounted() {
    this.setTest('bar'); // no type error
  }
}

I checked that behavior in fresh installed VSCode. And build process type check is passed too.

Change plugins order inside createStore method in order to use getters injected by custom plugins inside modules

First of all, @ktsn I would like to thank you for this package, it makes writing and maintaining app store code way much easier and fun.


I have a plugin that adds a new property to store.getters. This property should be obtained from store argument inside $init method in several modules' getters and actions objects.

In current version of the package this cannot be done: when $init is being executed, the property is not injected yet and remains undefined.

Changing plugins order at line 20 of src/index.ts solves the problem (plugins: (options.plugins || []).concat([injectStore]),) โ€” with this change all custom plugins are being executed before $init methods inside getters and actions objects.

It seems like an annoying bug but I'll still ask if maybe this was made intentionally?

Problem on this.getters when injecting mock state

Hi! I'm trying to inject mock state into getters which calls this.getters like this:

const mockedFooGetters = inject(FooGetters, {
    state: {
        count: 2,
    },
})

expect(mockedFooGetters.double()).toEqual(4);
export class FooGetters extends Getters<FooState> {
    public count(): number {
        return this.state.count;
    }

    public double(): number {
        return this.getters.count() * 2;
    }
}

But got an error:

    TypeError: Cannot read property 'getters' of undefined

       |
       |     public double(): number {
    >  |         return this.getters.count() * 2;
       |                     ^
       |     }
       | }
       |

on getters.

How can I solve this problem?

Include examples

Hi, nice job here, would love to see an examples with some ways of usage of Vuex modules.

Regards

duplicate getter key: $init

I have several modules with namespaced: false.
Each of them has a $init method (in the getter class)
And when the application starts, there is the error "duplicate getter key: $ init" which is clear for me( the same names of getters->$init).
But I want namespaced to be false.

Using getter from mutation

Hi!
How can I use getter from mutation (for code reuse)?

In another words, how can I use getter function on temporary store state without memoization?

mapActions() give IDE Error

Hi,

I am trying to integrate this library into my Vue project and am getting IDE Error in VSCode

No overload matches this call

vue: 2.6.11
vuex: 3.1.3
vue-property-decorator: 8.4.1
typescript: 3.8.3

class AudioPlayerState {
...
}

class AudioPlayerActions extends Actions<AudioPlayerState, any, any, AudioPlayerActions> {
  $init() {...}
  play() {...}
  stop() {...}
}

export const audioPlayer = new Module({
  	state: AudioPlayerState,
	actions: AudioPlayerActions,
})

export const audioPlayerMappers = createMapper(audioPlayer);

store.index.ts

Vue.use(Vuex)

const root = new Module({
	modules: {
		audioPlayer,
	},
});

export const store = createStore(root);

Vue component

@Component({
	name: 'PhoneClient',
	methods: audioPlayerMappers.mapActions({
		play: 'play', <----- Error here
	})
})
export default class Client extends Vue {...}

Please guide me on how to solve this issue and if any more information is required

docs: Mention that there are no rootGetters in vuex-smart-module and what to use instead with example

What's wrong:

  1. I'm rewriting my Vuex store from vanilla Vuex to vuex-smart-module

  2. One of the vanilla getters in my app uses rootGetters.

  3. I can't rewrite the vanilla getter to vuex-smart-module because there's no this.rootGetters in class FooGetters extends Getters<FooState>.

  4. I CTRL+F the docs for 'rootGetters' and get 0 results.

// assume this is some deeply nested submodule
class MyState {
  count = 1
}

class MyGetters extends Getters<MyState> {
  get localizedCount() {
   //obviously, this won't work because `this.` only contains `state` and other getters from `MyGetters`
    this.rootGetters.localizationModule.isConvertToRomanNumerals ? toRoman(this.state.count) : this.state.count
  }
}

I assume this is the solution, right?
This is already in the docs but I guess it's worth mentioning that rootGetters are totally gone as this may come as a surprise to people who are used to the vanilla Vuex API.

Organize docs

As README is getting longer, we need to organize it with a docs generator like VuePress for readability.

Decorators support

What do you think about create decorators helpers for a binding store to components?

comparison of vuex-smart-module with vuex-module-decorators

Hello, Ktsn. It's great repo.
I just wanted to know about benefits of vuex-smart-module over vuex-module-decorators.
So many technologies, libs and approaches. So I just do not sure if I need to try to use vuex-smart-module in my next projects? Is it more comfortable API? better TS support? Or something else?

npm files is not the git files

https://registry.npmjs.org/vuex-smart-module/-/vuex-smart-module-0.2.5.tgz

when I download this tarball and check the lib/index.d.ts, the definition is old

look like this

import { Store, StoreOptions } from 'vuex';
import { Module } from './module';
export { Getters, Mutations, Actions } from './assets';
export { Dispatch, Commit, Context } from './context';
export { registerModule, unregisterModule } from './register';
export { Module };
export declare function createStore(rootModule: Module<any, any, any, any>, options?: StoreOptions<any>): Store<any>;

Document how to mock nested module

I have a module nested in my root module and one of the root getters calls one of the nested module's getters (via the context created in $init()).

Could you explain and document how to best mock that nested module and its getter?

Jest TS: mocking mutations in actions fails

Hello,

I am writing some Jest tests for a store module written with [email protected]. I've encountered a problem when testing actions that call a mutation via the this.mutations property.

VSCode reports a No overload matches this call. error when trying to inject a mutations property in the inject function.

Here is an example code:

import { Mutations, Actions, Getters, inject } from 'vuex-smart-module';

class DemoState {
  str = 'hello';
}

class DemoMutations extends Mutations<DemoState> {
  update(str: string) {
    this.state.str = str;
  }
}

class DemoActions extends Actions<
  DemoState,
  Getters<DemoState>,
  DemoMutations,
  DemoActions
> {
  update(payload: string) {
    this.mutations.update(payload);
  }
}

test('action update', () => {
  const update = jest.fn();
  const actions = inject(DemoActions, { // <-- ERROR HERE
    state: new DemoState(),
    mutations: {
      update,
    },
  });

  actions.update('world');
  expect(update).toHaveBeenCalledWith('world');
});

The error seems to bother just the TS checker, because Jest tests do not fail.

Any hint on how to solve the issue?

Vue 3 and typescript

What's about Vue 3 ? will you provide support in the future?
What need to do for provide typings inside components ?

declare module '@vue/runtime-core' {
// provide typings for this.$store
interface ComponentCustomProperties {
$store: Store;
// $store: Store;
}
}

vuex-shim.d.ts

Thank You!

"Module" is referenced directly or indirectly in its own type annotation

I created an test repo for exposing this issue, you can find it here: https://github.com/MateiNenciu/nuxt-vuex-smart-module-test

Problem is highlighted here: https://github.com/MateiNenciu/nuxt-vuex-smart-module-test/blob/master/store/modules/Login/actions.ts#L20 . I believe this issue appear because Login is listed as a nested module in Root. We import RootModule inside LoginModule because in root we have several mutations that might get called depending of the situation ( an error log for example, look in LoginModule actions at that addError call example).
This issue only appears in version 0.3.0, if you downgrade the package to 0.2.7 version it shows no error.

Is this a bug or we should move the logic from RootModule to another nested module ?

Actions from super class aren't being registered

Here is a simple use case

import {Actions as SmartActions, Module} from "vuex-smart-module";

abstract class Actions extends SmartActions {
    // this action won't be registered
    fetchItems() {
        // fetch code
    }
}

class ConcreteActions extends Actions {
    deleteItems() {
        
    }
}

export const someModule = new Module({
    actions: ConcreteActions
})

[vuex] unknown action type: someModule/fetchItems

Can't access subModules actions or mutations of a SubModule from context(store).modules

Hey, thanks for your last release allowing subModules access with symbols!

But there is a bug, we can't access actions or mutations of submodules with the context(store).modules.nameOfModule feature.
The only way to access it, is, with the option.modules!.nameOfModule.context(store) way.

subModulesInMountedHook

Here are the log of this mounted hook:
logOfSubModulesActions

As we can see the first way doesn't show anything when we try to show the actions of a submodules, it's the same for actions and mutations. (getters and state are shown)

BTW: On the readme, there is only the first way specified.

Root module issue

const store = createStore(RootModule, {
  strict: config.isDev,
  plugins: [persistLocal.vuexPersistence.plugin],
  mutations: { ... }
});

When i wanna create a custom mutation ( from plugin ) - Root module rewrite my root mutations. It's a big problem. How Can I create RESTORE_MUTATION method what provide from plugin?

From the RootModule, I cannot create this mutation, because of this mutation must call this.$set method.

But, when I use the classic initialize store - it works is fine

new Vuex.Store({
  state: {
    count: 1,
  },
  mutations: {
    RESTORE_MUTATION: persistLocal.vuexPersistence.RESTORE_MUTATION,
  },
  plugins: [persistLocal.vuexPersistence.plugin],
});

How we can solve this problem?

`Error: An accessor cannot be declared in an ambient context

Hi.
I have an Error when trying to run my SPA. Can you tell me how to resolve it?

ERROR in [at-loader] ./node_modules/vuex-smart-module/lib/assets.d.ts:22:19
TS1086: An accessor cannot be declared in an ambient context.

and so on

ERROR in [at-loader] ./node_modules/vuex-smart-module/lib/context.d.ts:40:9
TS1086: An accessor cannot be declared in an ambient context.

I have installed: "typescript": "version": "3.5.1"

How to use with vue-class-component?

I'm trying to do something like that:

import foo from 'store/modules/foo';

@Component({
  methods: foo.mapMutations(['bar', 'baz'])
})

But got an error: Argument types do not match parameters on mapMutations.

Access "shared subModule" methods inside components (with context feature) from its parent Module

Hey, firstly I would like to thank you for this lib, it's the best way we found to use vuex in typescript !

To access part of a store we use context, but it seems we can't access subModule (state, mutation, action) with the context vuex-smart-module feature.

To illustrate the problem we have two main modules:

const homeStore = new Module({
  state: HomeState,
  getters: HomeGetter,
  mutations: HomeMutation,
  modules: {
    homeNodeStore: nodeStore.clone(),
    homeMemberStore: memberStore.clone()
  }
});
const projectStore = new Module({
  state: ProjectState,
  getters: ProjectGetter,
  mutations: ProjectMutation,
  modules: {
   projectNodeStore: nodeStore.clone(),
   projectMemberStore: memberStore.clone()
  }
});

As you can see Home and Project modules have each the two same sub-modules of the same type: nodeStore and memberStore (with different instances).

So how can we access (state, mutations, actions) of these sub modules from their parents using the typescript vuex-smart-module context, as we do when accessing (state, getter etc) directly through module:

homeStore.context(this.$store).(state, mutation, etc);

A tentative way to access subModule specific instance would be:

projectStore.nodeStore.context(this.$store).(state, mutation, etc);

or with the other module:

homeStore.nodeStore.context(this.$store).(state, mutation, etc);

In js vuex syntax we could do for example:

this.$store.commit("projectStore/nodeStore/theMutation", payload)

This feature is very important when we want to share store(s) implementation (used as subStore) between multiples stores.

BTW: I first try to implement this store code implementation sharing through inheritance (of each part of a store, state, mutation, action, etc), but because of the one single extend limitation we can't do that when we want to extend multiple store (here nodeStore and memberStore).

Hoping this feature can be resolved as it would allow vuex(-smart-module) to be even better !


Edit:

I just found a way to access the subStore from the parent with the ModuleOptions class accessible from the options property inside the Module class. You can simply do:

this.nodeState = homeStore.options.modules!["nodeStore"].context(this.$store).state;

But is there a way to access the subStore from its parents with a symbol and not by dereferencing the modules map by the name of the subStore ( which can cause runtime errors if the name is incorrect:( ) ?

Maybe this part should be more specifed in the readMe?

Issues with HMR in Nuxt

Hello, thank you for this great project!

I was trying to implement v0.4.0 in a Nuxt.js project using the store in module mode. Following your instructions, I was able to make it work perfectly. Anyway, I found an issue with HMR where, I believe, the accessors created for getters, mutations, and actions lose their this reference.

I created a small demo with a module and root store here: https://github.com/dwightjack/nuxt-vsm-demo

Implementation

I have created a demo module, registered it on the root store and then mapped one of its actions in a page component.

Step to reproduce

  1. run the development env: npm i && npm run dev
  2. on the homepage of the application write something in the input text. Everything works as expected
  3. edit either the store index or the module file and save
  4. HMR reloads the store
  5. write again in the input box: I receive the following error:

image

Notes

Looking at vuex-smart-module custom HMR function I guess that the problem is that Nuxt default implementation does not re-run the store plugins after reloading which is needed by this library to define the getters, mutations and actions accessors. But I don't know if I can implement a different HMR strategy since that logic is implemented in a store.js file inside the .nuxt folder.

Any hint on where could I look to find a fix for this problem?

Thanks in advance!

Mistake type

In first screen I response promise<string>
Screenshot from 2019-04-25 19-35-31

In component I see next response from this action:
Screenshot from 2019-04-25 19-35-41

Can we fix it?

Apply deep partial to get rid of @ts-ignore or casting any on mocking nested deps

https://github.com/ktsn/vuex-smart-module#mocking-nested-modules-and-dependencies

The current way to mock nested dependencies of getters and actions are a bit dirty. As the users want to partially apply mocked dependencies, they always need to ignore the type mismatch with @ts-ignore or casting the mock with any.

It should be not so difficult as there are ideas to implement DeepPartial interface in TypeScript on the Internet.

Dynamic register module

  1. How we can be register module dynamic?
  2. How use strong typing if I wanna use vuex in vue-router?

Bug with mutations in nested modules

Hi! I find some bug when I tried call mutations in nested vuex-smart-module. Submodule does not take into account full namespace.

Link for reproduction: https://github.com/Gimanh/vuex-smart

Reproduction:

  1. Clone and install reps.
  2. See code in App.ts hook created.
  3. Run npm run serve.
  4. On the main page press Press to increment count Foo will work increment, then press Press to increment count Bar and see error in console.

Composition API?

Is that possible (or how to do that in proper way?) to use vuex-smart-module with new Composition API (as Vue 2 plugin)?

How to use with vuex-persist

How you your package with plugins ( for example: vuex-persit ) when I need SHARED_MUTATION ?
Because, my root module replace all mutations in createStore

Add runtime warning for inappropriate usage

Usages which Vuex discourage and cannot prevent by type check should be warned on runtime.

The usages are:

  • To call a mutation method in another mutation.
  • To call a getter method / property directly in another getter.
  • To call an action method directly in another action.

tyscript lint error

vuex version 3.1.1
tyscript lint error

`vuex-smart-module/lib/assets.d.ts(1,10):
has no exported member 'Store'.

1 | import { Store } from 'vuex';
| ^
2 | import { Commit, Dispatch } from './context';
3 | import { MappedFunction } from './module';
4 | interface Class {

vuex-smart-module/lib/context.d.ts(1,10):
has no exported member 'Store'.

1 | import { Store, CommitOptions, DispatchOptions } from 'vuex';
| ^
2 | import { Payload, Dispatcher, Committer } from './assets';
3 | import { Module } from './module';
4 | export interface Commit {

vuex-smart-module/lib/context.d.ts(1,17):
has no exported member 'CommitOptions'.

1 | import { Store, CommitOptions, DispatchOptions } from 'vuex';
| ^
2 | import { Payload, Dispatcher, Committer } from './assets';
3 | import { Module } from './module';
4 | export interface Commit {

vuex-smart-module/lib/context.d.ts(1,32):
has no exported member 'DispatchOptions'.

1 | import { Store, CommitOptions, DispatchOptions } from 'vuex';
| ^
2 | import { Payload, Dispatcher, Committer } from './assets';
3 | import { Module } from './module';
4 | export interface Commit {

vuex-smart-module/lib/index.d.ts(1,10):
has no exported member 'Store'.

1 | import { Store, StoreOptions } from 'vuex';
| ^
2 | import { Module } from './module';
3 | export { Getters, Mutations, Actions, inject } from './assets';
4 | export { Dispatch, Commit, Context } from './context';

vuex-smart-module/lib/index.d.ts(1,17):
has no exported member 'StoreOptions'.

1 | import { Store, StoreOptions } from 'vuex';
| ^
2 | import { Module } from './module';
3 | export { Getters, Mutations, Actions, inject } from './assets';
4 | export { Dispatch, Commit, Context } from './context';

1 | import { Store, ModuleOptions } from 'vuex';
| ^
2 | import { Module } from './module';
3 | export declare function registerModule(store: Store, path: string | string[], namespace: string | null, module: Module<any, any, any, any>, options?: ModuleOptions): void;
4 | export declare function unregisterModule(store: Store, module: Module<any, any, any, any>): void;

vuex-smart-module/lib/register.d.ts(1,17):
has no exported member 'ModuleOptions'.

1 | import { Store, ModuleOptions } from 'vuex';
| ^
2 | import { Module } from './module';
3 | export declare function registerModule(store: Store, path: string | string[], namespace: string | null, module: Module<any, any, any, any>, options?: ModuleOptions): void;
4 | export declare function unregisterModule(store: Store, module: Module<any, any, any, any>): void;
`

maybe
import { Store } from 'vuex/types'
???

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.