diff --git a/docs/pages/en/2.accessor/2.dynamic-modules.md b/docs/pages/en/2.accessor/2.dynamic-modules.md index 132efad..ebbb41a 100644 --- a/docs/pages/en/2.accessor/2.dynamic-modules.md +++ b/docs/pages/en/2.accessor/2.dynamic-modules.md @@ -5,7 +5,9 @@ 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 @@ -21,7 +23,7 @@ export const mutations = mutationTree(state, { }) ``` -## Accessing the module +### Accessing the module You might want to use the store @@ -29,7 +31,7 @@ You might want to use the store 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) @@ -59,5 +61,106 @@ 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' +import * as myDynamicModuleNoSpread from '~/modules/myImportedModuleNoSpread' + +export const accessorType = getAccessorType({ + state, + getters, + mutations, + actions, + modules: { + submodule: { + //add this for module to show as possibly-undefined when using $accessor + dynamic: true, + namespaced: true, + ...myImportedModule, + modules: { + myNestedModule, + }, + }, + //You can add `export const dynamic = true;` inside module so you don't need to use spread operator in that case + //View the example below + submoduleWithoutSpread: myDynamicModuleNoSpread, + }, +}) +``` + +```ts{}[modules/myImportedModuleNoSpread.ts] +export const dynamic = true; + +export const state = () => ({ + //... +}); +``` + +### 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('my@email.com') + } + } + } +}) +``` + +### Typed $accessor methods +If you want methods `$accessorRegisterModule` and `$accessorUnregisterModule` to be typed, please add `nuxt-typed-vuex` package to `tsconfig.json` types. + +```json{}[tsconfig.json] +{ + { + "types": [ + "nuxt-typed-vuex" + ] + } +} ``` diff --git a/packages/nuxt-typed-vuex/src/index.ts b/packages/nuxt-typed-vuex/src/index.ts index 4358952..ea0f1e3 100644 --- a/packages/nuxt-typed-vuex/src/index.ts +++ b/packages/nuxt-typed-vuex/src/index.ts @@ -24,4 +24,5 @@ const nuxtTypedVuex: Module = function nuxtTypedVuex() { ;(nuxtTypedVuex as any).meta = { name, version } +export * from './types/accessorRegisterModule' export default nuxtTypedVuex diff --git a/packages/nuxt-typed-vuex/src/types/accessorRegisterModule.ts b/packages/nuxt-typed-vuex/src/types/accessorRegisterModule.ts new file mode 100644 index 0000000..f0884dc --- /dev/null +++ b/packages/nuxt-typed-vuex/src/types/accessorRegisterModule.ts @@ -0,0 +1,31 @@ +import type {Module} from "vuex"; + +type AccessorRegisterModule = (path: string | [string, ...string[]], module: Module) => 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 { + $accessorRegisterModule: AccessorRegisterModule; + $accessorUnregisterModule: AccessorUnregisterModule; + } +} diff --git a/packages/nuxt-typed-vuex/template/plugin.js b/packages/nuxt-typed-vuex/template/plugin.js index 6d8b136..b6471a1 100644 --- a/packages/nuxt-typed-vuex/template/plugin.js +++ b/packages/nuxt-typed-vuex/template/plugin.js @@ -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) + }) } diff --git a/packages/typed-vuex/src/accessor.ts b/packages/typed-vuex/src/accessor.ts index f81f141..7ed7621 100644 --- a/packages/typed-vuex/src/accessor.ts +++ b/packages/typed-vuex/src/accessor.ts @@ -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' @@ -97,3 +97,72 @@ export const getAccessorFromStore = (pattern: any) => { return (store: Store) => useAccessor(store, pattern._modules.root._rawModule) } + +const processModuleByPath = ( + accessor: MergedStoreType> & BlankStore, string>, + path: string | string[] +): {target: Record, 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, + accessor: MergedStoreType> & BlankStore, string>, + module: Module +) => { + 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, { + preserveState, + }) + processedModule.target[processedModule.key] = useAccessor(store, module, paths.join('/')) +} + +export const unregisterModule = ( + path: string | [string, ...string[]], + store: Store, + accessor: MergedStoreType> & BlankStore, string> +) => { + const processedModule = processModuleByPath(accessor, path) + store.unregisterModule(path as string) + delete processedModule.target[processedModule.key] +} diff --git a/packages/typed-vuex/src/index.ts b/packages/typed-vuex/src/index.ts index bb83391..28272e1 100644 --- a/packages/typed-vuex/src/index.ts +++ b/packages/typed-vuex/src/index.ts @@ -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' diff --git a/packages/typed-vuex/src/types/modules.ts b/packages/typed-vuex/src/types/modules.ts index 2dbf0fe..15afc24 100644 --- a/packages/typed-vuex/src/types/modules.ts +++ b/packages/typed-vuex/src/types/modules.ts @@ -2,6 +2,8 @@ import { NuxtStore, MergedStoreType, BlankStore } from './store' export type NuxtModules = Record> +type TransformedModule = MergedStoreType + export type ModuleTransformer = T extends NuxtModules - ? { [P in keyof T]: MergedStoreType } + ? { [P in keyof T]: 'dynamic' extends keyof T[P] ? undefined | TransformedModule : TransformedModule } : {} diff --git a/packages/typed-vuex/src/types/store.ts b/packages/typed-vuex/src/types/store.ts index 2a109f0..dd9cd2c 100644 --- a/packages/typed-vuex/src/types/store.ts +++ b/packages/typed-vuex/src/types/store.ts @@ -16,6 +16,7 @@ export interface BlankStore { mutations: {} actions: {} modules: {} + dynamic: boolean namespaced: boolean strict: boolean } @@ -26,6 +27,7 @@ export interface NuxtStore { mutations: Record actions: Record modules: NuxtModules + dynamic: boolean namespaced: boolean strict: boolean } @@ -37,6 +39,7 @@ export interface NuxtStoreInput< A, S extends { [key: string]: Partial } > { + dynamic?: boolean strict?: boolean namespaced?: boolean state: T diff --git a/packages/typed-vuex/test/accessor.test.ts b/packages/typed-vuex/test/accessor.test.ts index e3944b7..6ba8f29 100644 --- a/packages/typed-vuex/test/accessor.test.ts +++ b/packages/typed-vuex/test/accessor.test.ts @@ -1,4 +1,4 @@ -import { useAccessor, getAccessorType, getAccessorFromStore } from '../src' +import { useAccessor, getAccessorType, getAccessorFromStore, registerModule, unregisterModule } from '../src' import Vuex, { Store } from 'vuex' import Vue from 'vue' @@ -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) @@ -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) + }) })