Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #266: Add support of (un)registering global dynamic modules #267

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
85 changes: 81 additions & 4 deletions docs/pages/en/2.accessor/2.dynamic-modules.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
---
title: Dynamic modules
title: Dynamic modules description: 'Vanilla, strongly-typed store accessor.'
daniluk4000 marked this conversation as resolved.
Show resolved Hide resolved
description: 'Vanilla, strongly-typed store accessor.'
---

You can also use `typed-vuex` with dynamic modules.

## Sample module
## Variable usage

### Sample module

```ts{}[modules/dynamic-module.ts]
export const namespaced = true
Expand All @@ -21,15 +23,15 @@ export const mutations = mutationTree(state, {
})
```

## Accessing the module
### Accessing the module

You might want to use the store

```ts{}[components/my-component.vue]
import Vue from 'vue

import { useAccessor, getAccessorType } from 'typed-vuex'
import dynamicModule from '~/modules/dynamic-module'
import * as dynamicModule from '~/modules/dynamic-module'

const accessorType = getAccessorType(dynamicModule)

Expand Down Expand Up @@ -59,5 +61,80 @@ export default Vue.extend({
}
}
})
```

## Global usage

You can also register dynamic module on global level for your accessor. This can be a risky way, but can also be a way
for large projects with no able of using `useAccessor` as variable.

### Module definition

```ts{}[store/index.ts]
import * as myImportedModule from '~/modules/myImportedModule'
import * as myNestedModule from '~/modules/myNestedModule'

export const accessorType = getAccessorType({
state,
getters,
mutations,
actions,
modules: {
submodule: {
//add this for module to show as possibly-undefined when using $accessor
dynamic: true,
...myImportedModule,
modules: {
myNestedModule,
},
},
},
})
```

### Modules registration

```ts{}[components/my-component.vue]
import Vue from 'vue

//Import from typed-vuex is not required in Nuxt
import { registerModule, unregisterModule } from 'typed-vuex'
import myImportedModule from '~/modules/myImportedModule'
import myNestedModule from '~/modules/myNestedModule'

export default Vue.extend({
beforeCreated() {
//Or this.$accessorRegisterModule('myImportedModule', myImportedModule) in Nuxt
registerModule('myImportedModule', this.$store, this.$accessor, myImportedModule)

//Or this.$accessorRegisterModule(['myImportedModule', 'myNestedModule'], myImportedModule) in Nuxt
registerModule(['myImportedModule', 'myNestedModule'], this.$store, this.$accessor, myNestedModule)

//You can also register two modules at once, but you MUST include namespaced: true in that case
//this.$accessorRegisterModule('myImportedModule', {...}) syntax in Nuxt
registerModule('myImportedModule', {
...myImportedModule,
modules: {
myNestedModule: {
//required for correct usage, especially in Nuxt
namespaced: true,
...myNestedModule,
},
},
})

//To unregister module(s), simply call this
//this.$accessorUnregisterModule('myImportedModule') in Nuxt
unregisterModule('myImportedModule', this.$store, this.$accessor)
},
methods: {
anotherMethod() {
//If you are not sure about if module will always exist,
// it is recommended to enable dynamic: true in store and always check for undefined
if (this.accessor) {
this.$accessor.myImportedModule?.myNestedModule?.addEmail('[email protected]')
}
}
}
})
```
1 change: 1 addition & 0 deletions packages/nuxt-typed-vuex/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ const nuxtTypedVuex: Module = function nuxtTypedVuex() {

;(nuxtTypedVuex as any).meta = { name, version }

export * from './types/accessorRegisterModule'
export default nuxtTypedVuex
31 changes: 31 additions & 0 deletions packages/nuxt-typed-vuex/src/types/accessorRegisterModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type {Module} from "vuex";

type AccessorRegisterModule = (path: string | [string, ...string[]], module: Module<any, any>) => void
type AccessorUnregisterModule = (path: string | [string, ...string[]]) => void

declare module '@nuxt/types' {
interface NuxtAppOptions {
$accessorRegisterModule: AccessorRegisterModule;
$accessorUnregisterModule: AccessorUnregisterModule;
}

interface Context {
$accessorRegisterModule: AccessorRegisterModule;
$accessorUnregisterModule: AccessorUnregisterModule;
}
}

declare module 'vue/types/vue' {
interface Vue {
$accessorRegisterModule: AccessorRegisterModule;
$accessorUnregisterModule: AccessorUnregisterModule;
}
}

declare module 'vuex/types/index' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Store<S> {
$accessorRegisterModule: AccessorRegisterModule;
$accessorUnregisterModule: AccessorUnregisterModule;
}
}
11 changes: 9 additions & 2 deletions packages/nuxt-typed-vuex/template/plugin.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { getAccessorFromStore } from 'typed-vuex'
import { getAccessorFromStore, registerModule, unregisterModule } from 'typed-vuex'

import { createStore } from '<%= options.store %>'

const storeAccessor = getAccessorFromStore(createStore())

export default async ({ store }, inject) => {
inject('accessor', storeAccessor(store))
const accessor = storeAccessor(store)
inject('accessor', accessor)
inject('accessorRegisterModule', (path, module) => {
registerModule(path, store, accessor, module)
})
inject('accessorUnregisterModule', (path) => {
unregisterModule(path, store, accessor)
})
}
73 changes: 71 additions & 2 deletions packages/typed-vuex/src/accessor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Store, GetterTree, MutationTree, ActionTree } from 'vuex'
import { NuxtStoreInput, MergedStoreType, BlankStore } from './types/store'
import { ActionTree, GetterTree, Module, MutationTree, Store } from 'vuex'
import { BlankStore, MergedStoreType, NuxtStoreInput } from './types/store'
import { State, StateType } from './types/state'
import { NuxtModules } from './types/modules'

Expand Down Expand Up @@ -97,3 +97,72 @@ export const getAccessorFromStore = (pattern: any) => {
return (store: Store<any>) =>
useAccessor(store, pattern._modules.root._rawModule)
}

const processModuleByPath = (
accessor: MergedStoreType<Partial<NuxtStoreInput<any, any, any, any, any>> & BlankStore, string>,
path: string | string[]
): {target: Record<string, any>, key: string} => {
const paths = typeof path === 'string' ? [path] : path

let target = accessor
let key: string | undefined
paths.forEach((part, index) => {
if (index === paths.length - 1) {
if (!target) throw new Error(`Could not find parent module for ${paths[index - 1] || paths[index]}`)
key = part
} else {
target = target[part]
}
})

return {
target,
//Key can not be undefined
key: key as string,
}
}

export const registerModule = (
path: string | [string, ...string[]],
store: Store<any>,
accessor: MergedStoreType<Partial<NuxtStoreInput<any, any, any, any, any>> & BlankStore, string>,
module: Module<any, any>
) => {
module.namespaced = true

if (module.modules) module.modules = Object.entries(module.modules).reduce((acc, [key, value]) => ({
...acc,
[key]: {
...value,
namespaced: true,
}
}), {}) as typeof module['modules']

let preserveState = false
if (typeof path === 'string') preserveState = !!store.state[path]
else {
let target = store.state
for (const key of path) {
if (!target) break
target = target[key]
}
preserveState = !!target
}

const paths = typeof path === 'string' ? [path] : path
const processedModule = processModuleByPath(accessor, paths)
store.registerModule(path as string, module as Module<any, any>, {
preserveState,
})
processedModule.target[processedModule.key] = useAccessor(store, module, paths.join('/'))
}

export const unregisterModule = (
path: string | [string, ...string[]],
store: Store<any>,
accessor: MergedStoreType<Partial<NuxtStoreInput<any, any, any, any, any>> & BlankStore, string>
) => {
const processedModule = processModuleByPath(accessor, path)
store.unregisterModule(path as string)
delete processedModule.target[processedModule.key]
}
2 changes: 1 addition & 1 deletion packages/typed-vuex/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ export * from './types/actions'
export * from './types/getters'
export * from './types/mutations'
export * from './types/utils'
export { useAccessor, getAccessorType, getAccessorFromStore } from './accessor'
export { useAccessor, getAccessorType, getAccessorFromStore, registerModule, unregisterModule } from './accessor'
export { createMapper } from './utils'
6 changes: 4 additions & 2 deletions packages/typed-vuex/src/types/modules.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { NuxtStore, MergedStoreType, BlankStore } from './store'

export type NuxtModules = Record<string, Partial<NuxtStore>>
export type NuxtModules = Record<string, Partial<NuxtStore & {dynamic: boolean}>>

type TransformedModule<T extends NuxtModules, P extends keyof T, O = string> = MergedStoreType<T[P] & BlankStore, O>

export type ModuleTransformer<T, O = string> = T extends NuxtModules
? { [P in keyof T]: MergedStoreType<T[P] & BlankStore, O> }
? { [P in keyof T]: T[P]['dynamic'] extends boolean ? undefined | TransformedModule<T, P, O> : TransformedModule<T, P, O> }
: {}
6 changes: 5 additions & 1 deletion packages/typed-vuex/src/types/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ export interface NuxtStore {
modules: NuxtModules
}

export interface ExtendedNuxtStore extends NuxtStore {
dynamic: boolean;
}

export interface NuxtStoreInput<
T extends State,
G,
M,
A,
S extends { [key: string]: Partial<NuxtStore> }
S extends { [key: string]: Partial<ExtendedNuxtStore> }
> {
namespaced?: boolean
state: T
Expand Down
35 changes: 33 additions & 2 deletions packages/typed-vuex/test/accessor.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useAccessor, getAccessorType, getAccessorFromStore } from 'typed-vuex'
import { useAccessor, getAccessorType, getAccessorFromStore, registerModule, unregisterModule } from 'typed-vuex'
import Vuex, { Store } from 'vuex'
import Vue from 'vue'

Expand Down Expand Up @@ -120,7 +120,7 @@ describe('accessor', () => {
test('namespaced dynamic modules work', async () => {
store.registerModule('dynamicSubmodule', { ...submodule, namespaced: true })
const dynamicAccessor = useAccessor(store, {
modules: { dynamicSubmodule: { ...submodule, namespaced: true } },
modules: {dynamicSubmodule: { ...submodule, namespaced: true }},
})

submoduleBehaviour(dynamicAccessor.dynamicSubmodule)
Expand All @@ -134,4 +134,35 @@ describe('accessor', () => {
submoduleBehaviour(dynamicAccessor)
store.unregisterModule('dynamicSubmodule')
})
test('dynamic module global registration works', async () => {
registerModule('dynamicSubmodule', store, accessor, submodule)
expect(store.state.dynamicSubmodule.firstName).toBeDefined()
expect(store.state.dynamicSubmodule.nestedSubmodule).toBeUndefined()
submoduleBehaviour(accessor.dynamicSubmodule)
expect(accessor.dynamicSubmodule.nestedSubmodule).toBeUndefined()

registerModule(['dynamicSubmodule', 'nestedSubmodule'], store, accessor, submodule)
expect(store.state.dynamicSubmodule.nestedSubmodule.firstName).toBeDefined()
submoduleBehaviour(accessor.dynamicSubmodule.nestedSubmodule)

unregisterModule('dynamicSubmodule', store, accessor)
expect(store.state.dynamicSubmodule).toBeUndefined()
expect(accessor.dynamicSubmodule).toBeUndefined()

registerModule('dynamicSubmodule', store, accessor, submodule)
expect(store.state.dynamicSubmodule.nestedSubmodule).toBeUndefined()
expect(accessor.dynamicSubmodule.nestedSubmodule).toBeUndefined()

registerModule(['dynamicSubmodule', 'nestedSubmodule'], store, accessor, submodule)
expect(store.state.dynamicSubmodule.nestedSubmodule.firstName).toBeDefined()
expect(accessor.dynamicSubmodule.nestedSubmodule.firstName).toBeDefined()

unregisterModule(['dynamicSubmodule', 'nestedSubmodule'], store, accessor)
expect(store.state.dynamicSubmodule).toBeDefined()
expect(accessor.dynamicSubmodule).toBeDefined()
expect(store.state.dynamicSubmodule.nestedSubmodule).toBeUndefined()
expect(accessor.dynamicSubmodule.nestedSubmodule).toBeUndefined()

unregisterModule('dynamicSubmodule', store, accessor)
})
})