Skip to content

Commit

Permalink
Added support for root everything and nested modules
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Crowl committed Jul 10, 2017
1 parent 57c4057 commit 01e9efd
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 47 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vuex-typex",
"version": "1.0.8",
"version": "1.0.9",
"description": "A TypeScript pattern for strongly-typed access to Vuex Store modules",
"files": [
"dist/index.js",
Expand Down
190 changes: 147 additions & 43 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type MutationHandler<S, P> = (state: S, payload: P) => void
export type ActionHandler<S, R, P, T> = (context: BareActionContext<S, R>, payload: P) => Promise<T> | T
export type GetterHandler<S, R, T> = (state: S, rootState: R) => T


interface Dictionary<T> { [key: string]: T }
interface RootStore<R> extends Store<R> { rootGetters?: any }

export interface BareActionContext<S, R>
Expand All @@ -16,19 +18,66 @@ export interface BareActionContext<S, R>
}

class ModuleBuilderImpl<S, R={}> implements ModuleBuilder<S, R> {
private _store: RootStore<R>
protected _store: RootStore<R> | undefined

private _getters: GetterTree<S, R> = {}
private _mutations: MutationTree<S> = {}
private _actions: ActionTree<S, R> = {}
protected _getters: GetterTree<S, R> = {}
protected _mutations: MutationTree<S> = {}
protected _actions: ActionTree<S, R> = {}
protected _moduleBuilders: Dictionary<ModuleBuilder<any, R>> = {}

private _vuexModule: Module<S, R> | undefined
protected _vuexModule: Module<S, R> | undefined

constructor(public readonly namespace: string, private _state: S) { }
constructor(public readonly namespace: string, private _initialState: S) { }

state(): () => S
{
return () => (<any>this._store.state)[this.namespace] as S
if (!this.namespace)
{
return () => <any>this._store!.state as S
}
else if (this.namespace.indexOf("/") < 0)
{
return () => (<any>this._store!.state)[this.namespace] as S
}
else
{
const namespaces = this.namespace.split("/")
return () =>
{
let accessor: any = this._store!.state
for (const name of namespaces)
{
accessor = accessor[name]
}
return (<any>accessor) as S
}
}
}

module<M>(namespace: string, initialState: M): ModuleBuilder<M, R>
module<M>(namespace: string): ModuleBuilder<M, R>
module<M>(namespace: string, initialState?: M): ModuleBuilder<M, R>
{
const existingModule = this._moduleBuilders[namespace]
const qualifiedNamespace = qualifyNamespace(this.namespace, namespace)
if (!initialState)
{
// no second argument: get an existing module
if (!existingModule)
{
throw new Error(`There is no module named '${qualifiedNamespace}'. If you meant to create a nested module, then provide initial-state as the second argument.'`)
}
return existingModule
}

// both arguments: create a module
if (existingModule)
{
throw new Error(`There is already a module named '${qualifiedNamespace}'. If you meant to get the existing module, then provide no initialState argument.`)
}
const nestedBuilder = new ModuleBuilderImpl(qualifiedNamespace, initialState)
this._moduleBuilders[namespace] = nestedBuilder
return nestedBuilder
}

commit<P>(handler: MutationHandler<S, void>): () => void
Expand All @@ -43,7 +92,7 @@ class ModuleBuilderImpl<S, R={}> implements ModuleBuilder<S, R> {
throw new Error(`There is already a mutation named ${key}.`)
}
this._mutations[key] = handler
return ((payload: P) => this._store.commit(namespacedKey, payload, useRootNamespace)) as any
return ((payload: P) => this._store!.commit(namespacedKey, payload, useRootNamespace)) as any
}

dispatch<P, T>(handler: ActionHandler<S, R, void, void>): () => Promise<void>
Expand All @@ -62,7 +111,7 @@ class ModuleBuilderImpl<S, R={}> implements ModuleBuilder<S, R> {
throw new Error(`There is already an action named ${key}.`)
}
this._actions[key] = handler
return (payload: P) => this._store.dispatch(namespacedKey, payload, useRootNamespace)
return (payload: P) => this._store!.dispatch(namespacedKey, payload, useRootNamespace)
}

read<T>(handler: GetterHandler<S, R, T>): () => T
Expand All @@ -77,24 +126,32 @@ class ModuleBuilderImpl<S, R={}> implements ModuleBuilder<S, R> {
this._getters[key] = handler
return () =>
{
if (this._store.rootGetters)
if (this._store!.rootGetters)
{
return this._store.rootGetters[namespacedKey] as T
return this._store!.rootGetters[namespacedKey] as T
}
return this._store.getters[namespacedKey] as T
return this._store!.getters[namespacedKey] as T
}
}

vuexModule(): Module<S, R>
{
if (!this._vuexModule)
{
// build nested modules recursively, if any
const modules: ModuleTree<R> = {}
for (const namespace of Object.keys(this._moduleBuilders))
{
modules[namespace] = this._moduleBuilders[namespace].vuexModule()
}

this._vuexModule = {
namespaced: true,
state: this._state,
state: this._initialState,
getters: this._getters,
mutations: this._mutations,
actions: this._actions
actions: this._actions,
modules
}
}
return this._vuexModule
Expand All @@ -103,6 +160,8 @@ class ModuleBuilderImpl<S, R={}> implements ModuleBuilder<S, R> {
_provideStore(store: Store<R>)
{
this._store = store

forEachValue(this._moduleBuilders, m => m._provideStore(store))
}
}

Expand All @@ -113,21 +172,32 @@ function qualifyKey(handler: Function, namespace: string | undefined, name?: str
{
throw new Error(`Vuex handler functions must not be anonymous. Possible causes: fat-arrow functions, uglify. To fix, pass a unique name as a second parameter after your callback.`)
}
return namespace ? { key, namespacedKey: `${namespace}/${key}` } : { key, namespacedKey: key }
return { key, namespacedKey: qualifyNamespace(namespace, key) }
}

function qualifyNamespace(namespace: string | undefined, key: string)
{
return namespace ? `${namespace}/${key}` : key
}

export interface ModuleBuilder<S, R={}>
{
/** The namespace of this ModuleBuilder */
readonly namespace: string

/** Returns a strongly-typed commit function for the provided mutation handler */
/** Creates a strongly-typed nested module within this module */
module<M>(namespace: string, initialState: M): ModuleBuilder<M, R>

/** Gets an existing nested module within this module */
module<M>(namespace: string): ModuleBuilder<M, R>

/** Creates a strongly-typed commit function for the provided mutation handler */
commit<P>(handler: MutationHandler<S, void>): () => void
commit<P>(handler: MutationHandler<S, P>): (payload: P) => void
commit<P>(handler: MutationHandler<S, void>, name: string): () => void
commit<P>(handler: MutationHandler<S, P>, name: string): (payload: P) => void

/** Returns a strongly-typed dispatch function for the provided action handler */
/** Creates a strongly-typed dispatch function for the provided action handler */
dispatch<P, T>(handler: ActionHandler<S, R, void, void>): () => Promise<void>
dispatch<P, T>(handler: ActionHandler<S, R, P, void>): (payload: P) => Promise<void>
dispatch<P, T>(handler: ActionHandler<S, R, void, T>): () => Promise<T>
Expand All @@ -137,76 +207,110 @@ export interface ModuleBuilder<S, R={}>
dispatch<P, T>(handler: ActionHandler<S, R, void, T>, name: string): () => Promise<T>
dispatch<P, T>(handler: ActionHandler<S, R, P, T>, name: string): (payload: P) => Promise<T>

/** Returns a strongly-typed read function for the provided getter function */
/** Creates a strongly-typed read function for the provided getter function */
read<T>(handler: GetterHandler<S, R, T>): () => T
read<T>(handler: GetterHandler<S, R, T>, name: string): () => T

/** Returns a method to return this module's state */
/** Creates a method to return this module's state */
state(): () => S

/** Returns a Vuex Module. Called after all strongly-typed functions have been obtained */
/** Output a Vuex Module definition. Called after all strongly-typed functions have been obtained */
vuexModule(): Module<S, R>

_provideStore(store: Store<R>): void
}

class StoreBuilderImpl<R> implements StoreBuilder<R> {
private _moduleBuilders: ModuleBuilder<any, R>[] = []
private _vuexStore: Store<R> | undefined

constructor() { }
class StoreBuilderImpl<R> extends ModuleBuilderImpl<any, R> {
constructor()
{
super("", {})
}

module<S>(namespace: string, state: S): ModuleBuilder<S, R>
module<S>(namespace: string, initialState: S): ModuleBuilder<S, R>
module<S>(namespace: string): ModuleBuilder<S, R>
module<S>(namespace: string, initialState?: S): ModuleBuilder<S, R>
{
if (this._vuexStore)
if (this._store && initialState)
{
throw new Error("Can't call module() after vuexStore() has been called")
throw new Error("Can't add module after vuexStore() has been called")
}
const builder = new ModuleBuilderImpl<S, R>(namespace, state)
this._moduleBuilders.push(builder)
return builder

return super.module(namespace, initialState) as ModuleBuilder<S, R>
}

vuexStore(): Store<R>
vuexStore(overrideOptions: StoreOptions<R>): Store<R>
vuexStore(overrideOptions: StoreOptions<R> = {}): Store<R>
{
if (!this._vuexStore)
if (!this._store)
{
const options: StoreOptions<R> = {
...this.createStoreOptions(),
const options: StoreOptions<R> = {
...this.vuexModule(),
...overrideOptions
}
const store = new Store<R>(options)
this._moduleBuilders.forEach(m => m._provideStore(store))
this._vuexStore = store
forEachValue(this._moduleBuilders, m => m._provideStore(store))
this._store = store
}
return this._vuexStore
return this._store
}

private createStoreOptions(): StoreOptions<R>
reset()
{
const modules: ModuleTree<R> = {}
this._moduleBuilders.forEach(m => modules[m.namespace] = m.vuexModule())
return { modules }
this._store = undefined
this._moduleBuilders = {}
}
}

const forEachValue = <T>(dict: Dictionary<T>, loop: (value: T) => any) =>
{
Object.keys(dict).forEach(key => loop(dict[key]))
}

export interface VuexStoreOptions<R>
{
plugins?: Plugin<R>[]
}

export interface StoreBuilder<R>
export interface StoreBuilder<R>
{
/** Get a ModuleBuilder for the namespace provided */
/** Creates a ModuleBuilder for the namespace provided */
module<S>(namespace: string, state: S): ModuleBuilder<S, R>

/** Gets an existing ModuleBuilder for the namespace provided */
module<S>(namespace: string): ModuleBuilder<S, R>

/** Output a Vuex Store after all modules have been built */
vuexStore(): Store<R>

/** Output a Vuex Store and provide options, e.g. plugins -- these take precedence over any auto-generated options */
vuexStore(overrideOptions: StoreOptions<R>): Store<R>

/** Creates a strongly-typed commit function for the provided mutation handler */
commit<P>(handler: MutationHandler<R, void>): () => void
commit<P>(handler: MutationHandler<R, P>): (payload: P) => void
commit<P>(handler: MutationHandler<R, void>, name: string): () => void
commit<P>(handler: MutationHandler<R, P>, name: string): (payload: P) => void

/** Creates a strongly-typed dispatch function for the provided action handler */
dispatch<P, T>(handler: ActionHandler<R, R, void, void>): () => Promise<void>
dispatch<P, T>(handler: ActionHandler<R, R, P, void>): (payload: P) => Promise<void>
dispatch<P, T>(handler: ActionHandler<R, R, void, T>): () => Promise<T>
dispatch<P, T>(handler: ActionHandler<R, R, P, T>): (payload: P) => Promise<T>
dispatch<P, T>(handler: ActionHandler<R, R, void, void>, name: string): () => Promise<void>
dispatch<P, T>(handler: ActionHandler<R, R, P, void>, name: string): (payload: P) => Promise<void>
dispatch<P, T>(handler: ActionHandler<R, R, void, T>, name: string): () => Promise<T>
dispatch<P, T>(handler: ActionHandler<R, R, P, T>, name: string): (payload: P) => Promise<T>

/** Creates a strongly-typed read function for the provided getter function */
read<T>(handler: GetterHandler<R, R, T>): () => T
read<T>(handler: GetterHandler<R, R, T>, name: string): () => T

/** Creates a method to return the root state */
state(): () => R

/** WARNING: Discards vuex store and reset modules (non intended for end-user use) */
reset(): void
}

const storeBuilderSingleton = new StoreBuilderImpl<any>()
Expand Down
4 changes: 3 additions & 1 deletion src/tests/anon-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ describe("Create an anon store", () =>
let moduleBuilder: ModuleBuilder<AnonState>
beforeEach(() =>
{
moduleBuilder = getStoreBuilder("anon").module("anon", { age: 36 })
const anonStore = getStoreBuilder("anon")
anonStore.reset()
moduleBuilder = anonStore.module("anon", { age: 36 })
})

describe("try to create a getter with anon function", () =>
Expand Down
35 changes: 35 additions & 0 deletions src/tests/nested.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect } from "chai"
import * as Vue from "vue"
import * as Vuex from "vuex"
import { buildStore } from "./store"
import { RootState } from "./store/index"
import { getStoreBuilder, StoreBuilder, ModuleBuilder } from "../index"
import { Store } from "vuex"

interface OuterState { str: string, inner: InnerState }
interface InnerState { int: number }

describe("Create a store", () =>
{
let outerBuilder: ModuleBuilder<OuterState>
let innerBuilder: ModuleBuilder<InnerState>
let storeBuilder: StoreBuilder<{}>
beforeEach(() =>
{
storeBuilder = getStoreBuilder("nested-store")
outerBuilder = storeBuilder.module("outer", <OuterState>{ str: "hello, world." })
innerBuilder = outerBuilder.module("inner", <InnerState>{ int: 42 })
// innerBuilder = storeBuilder.module("outer/inner", { int: 42 })
})

describe("that includes nested modules", () =>
{
it("should access nested value", () =>
{
const store = storeBuilder.vuexStore()
const readState = outerBuilder.state()

expect(readState().inner.int).to.equal(42)
})
})
})
4 changes: 2 additions & 2 deletions src/tests/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Store } from "vuex"

interface PluginState { age: number }

describe("Create a store with a plugin", () =>
describe("Create a store", () =>
{
let moduleBuilder: ModuleBuilder<PluginState>
let storeBuilder: StoreBuilder<{}>
Expand All @@ -25,7 +25,7 @@ describe("Create a store with a plugin", () =>
commitDecrease = moduleBuilder.commit((state, payload) => { state.age-- }, "decrease")
})

describe("create a store that includes a logger plugin", () =>
describe("that includes a logger plugin", () =>
{
it("should log each mutation", () =>
{
Expand Down
Loading

0 comments on commit 01e9efd

Please sign in to comment.