Skip to content

Commit 3b2a01d

Browse files
author
Ben Crowl
committed
Store builder plus a little coverage
1 parent 634bfdc commit 3b2a01d

11 files changed

+255
-76
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vuex-typex",
3-
"version": "1.0.0",
3+
"version": "1.0.2",
44
"description": "A TypeScript pattern for strongly-typed access to Vuex Store modules",
55
"files": [
66
"dist/index.js",

src/bah.ts

-11
This file was deleted.

src/index.ts

+128-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
import { ActionContext, ActionTree, GetterTree, Module, MutationTree, Store } from "vuex"
2+
import { ActionContext, ActionTree, GetterTree, Module, MutationTree, Store, StoreOptions, ModuleTree } from "vuex"
33

44
const useRootNamespace = { root: true }
55

@@ -15,14 +15,16 @@ export interface BareActionContext<S, R>
1515
rootState: R
1616
}
1717

18-
export class ModuleBuilder<S, R> {
19-
private store: RootStore<R>
18+
class ModuleBuilderImpl<S, R={}> implements ModuleBuilder<S, R> {
19+
private _store: RootStore<R>
2020

21-
private getters: GetterTree<S, R> = {}
22-
private mutations: MutationTree<S> = {}
23-
private actions: ActionTree<S, R> = {}
21+
private _getters: GetterTree<S, R> = {}
22+
private _mutations: MutationTree<S> = {}
23+
private _actions: ActionTree<S, R> = {}
2424

25-
constructor(private namespace: string, private state: S) { }
25+
private _vuexModule: Module<S, R> | undefined
26+
27+
constructor(public readonly namespace: string, private state: S) { }
2628

2729
commit<P>(handler: MutationHandler<S, void>): () => void
2830
commit<P>(handler: MutationHandler<S, P>): (payload: P) => void
@@ -31,8 +33,8 @@ export class ModuleBuilder<S, R> {
3133
commit<P>(handler: MutationHandler<S, P>, name?: string)
3234
{
3335
const { key, namespacedKey } = qualifyKey(handler, this.namespace, name)
34-
this.mutations[key] = handler
35-
return ((payload: P) => this.store.commit(namespacedKey, payload, useRootNamespace)) as any
36+
this._mutations[key] = handler
37+
return ((payload: P) => this._store.commit(namespacedKey, payload, useRootNamespace)) as any
3638
}
3739

3840
dispatch<P, T>(handler: ActionHandler<S, R, void, void>): () => Promise<void>
@@ -46,40 +48,44 @@ export class ModuleBuilder<S, R> {
4648
dispatch<P, T>(handler: any, name?: string): any
4749
{
4850
const { key, namespacedKey } = qualifyKey(handler, this.namespace, name)
49-
this.actions[key] = handler
50-
return (payload: P) => this.store.dispatch(namespacedKey, payload, useRootNamespace)
51+
this._actions[key] = handler
52+
return (payload: P) => this._store.dispatch(namespacedKey, payload, useRootNamespace)
5153
}
5254

5355
read<T>(handler: GetterHandler<S, R, T>): () => T
5456
read<T>(handler: GetterHandler<S, R, T>, name: string): () => T
5557
read<T>(handler: GetterHandler<S, R, T>, name?: string): () => T
5658
{
5759
const { key, namespacedKey } = qualifyKey(handler, this.namespace, name)
58-
this.getters[key] = handler
60+
this._getters[key] = handler
5961
return () =>
6062
{
61-
if (this.store.rootGetters)
63+
if (this._store.rootGetters)
6264
{
63-
return this.store.rootGetters[namespacedKey] as T
65+
return this._store.rootGetters[namespacedKey] as T
6466
}
65-
return this.store.getters[namespacedKey] as T
67+
return this._store.getters[namespacedKey] as T
6668
}
6769
}
6870

69-
provideStore(): (store: Store<R>) => void
71+
vuexModule(): Module<S, R>
7072
{
71-
return (store) => this.store = store
73+
if (!this._vuexModule)
74+
{
75+
this._vuexModule = {
76+
namespaced: true,
77+
state: this.state,
78+
getters: this._getters,
79+
mutations: this._mutations,
80+
actions: this._actions
81+
}
82+
}
83+
return this._vuexModule
7284
}
7385

74-
toVuexModule(): Module<S, R>
86+
_provideStore(store: Store<R>)
7587
{
76-
return {
77-
namespaced: true,
78-
state: this.state,
79-
getters: this.getters,
80-
mutations: this.mutations,
81-
actions: this.actions
82-
}
88+
this._store = store
8389
}
8490
}
8591

@@ -91,4 +97,101 @@ function qualifyKey(handler: Function, namespace: string | undefined, name?: str
9197
throw new Error(`Vuex handler functions must not be anonymous. Possible causes: fat-arrow functions, uglify`)
9298
}
9399
return namespace ? { key, namespacedKey: `${namespace}/${key}` } : { key, namespacedKey: key }
100+
}
101+
102+
export interface ModuleBuilder<S, R={}>
103+
{
104+
/** The namespace of this ModuleBuilder */
105+
readonly namespace: string
106+
107+
/** Returns a strongly-typed commit function for the provided mutation handler */
108+
commit<P>(handler: MutationHandler<S, void>): () => void
109+
commit<P>(handler: MutationHandler<S, P>): (payload: P) => void
110+
commit<P>(handler: MutationHandler<S, void>, name: string): () => void
111+
commit<P>(handler: MutationHandler<S, P>, name: string): (payload: P) => void
112+
113+
/** Returns a strongly-typed dispatch function for the provided action handler */
114+
dispatch<P, T>(handler: ActionHandler<S, R, void, void>): () => Promise<void>
115+
dispatch<P, T>(handler: ActionHandler<S, R, P, void>): (payload: P) => Promise<void>
116+
dispatch<P, T>(handler: ActionHandler<S, R, void, T>): () => Promise<T>
117+
dispatch<P, T>(handler: ActionHandler<S, R, P, T>): (payload: P) => Promise<T>
118+
dispatch<P, T>(handler: ActionHandler<S, R, void, void>, name: string): () => Promise<void>
119+
dispatch<P, T>(handler: ActionHandler<S, R, P, void>, name: string): (payload: P) => Promise<void>
120+
dispatch<P, T>(handler: ActionHandler<S, R, void, T>, name: string): () => Promise<T>
121+
dispatch<P, T>(handler: ActionHandler<S, R, P, T>, name: string): (payload: P) => Promise<T>
122+
123+
/** Returns a strongly-typed read function for the provided getter function */
124+
read<T>(handler: GetterHandler<S, R, T>): () => T
125+
read<T>(handler: GetterHandler<S, R, T>, name: string): () => T
126+
127+
/** Returns a Vuex Module. Called after all strongly-typed functions have been obtained */
128+
vuexModule(): Module<S, R>
129+
130+
_provideStore(store: Store<R>): void
131+
}
132+
133+
class StoreBuilderImpl<R> implements StoreBuilder<R> {
134+
private _moduleBuilders: ModuleBuilder<any, R>[] = []
135+
private _vuexStore: Store<R> | undefined
136+
137+
constructor() { }
138+
139+
module<S>(namespace: string, state: S): ModuleBuilder<S, R>
140+
{
141+
if (this._vuexStore)
142+
{
143+
throw new Error("Can't call module() after vuexStore() has been called")
144+
}
145+
const builder = new ModuleBuilderImpl<S, R>(namespace, state)
146+
this._moduleBuilders.push(builder)
147+
return builder
148+
}
149+
150+
vuexStore(): Store<R>
151+
{
152+
if (!this._vuexStore)
153+
{
154+
const options: StoreOptions<R> = this.createStoreOptions()
155+
const store = new Store<R>(options)
156+
this._moduleBuilders.forEach(m => m._provideStore(store))
157+
this._vuexStore = store
158+
}
159+
return this._vuexStore
160+
}
161+
162+
private createStoreOptions(): StoreOptions<R>
163+
{
164+
const modules: ModuleTree<R> = {}
165+
this._moduleBuilders.forEach(m => modules[m.namespace] = m.vuexModule())
166+
return { modules }
167+
}
168+
}
169+
170+
export interface StoreBuilder<R>
171+
{
172+
/** Get a ModuleBuilder for the namespace provided */
173+
module<S>(namespace: string, state: S): ModuleBuilder<S, R>
174+
175+
/** Output a Vuex Store after all modules have been built */
176+
vuexStore(): Store<R>
177+
}
178+
179+
const storeBuilderSingleton = new StoreBuilderImpl<any>()
180+
const namedStoreBuilderMap: { [name: string]: StoreBuilderImpl<any> } = Object.create(null)
181+
182+
/** Get a reference to the default store builder */
183+
export function getStoreBuilder<R>(): StoreBuilder<R>
184+
/** Get a reference to a named store builder */
185+
export function getStoreBuilder<R>(name: string): StoreBuilder<R>
186+
export function getStoreBuilder<R>(name?: string): StoreBuilder<R>
187+
{
188+
// the default store builder
189+
if (!name)
190+
{
191+
return storeBuilderSingleton
192+
}
193+
194+
// a named store builder
195+
const builder = namedStoreBuilderMap[name] || (namedStoreBuilderMap[name] = new StoreBuilderImpl<R>())
196+
return builder
94197
}

src/tests/anon-handler.spec.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { expect } from "chai"
2+
import * as Vue from "vue"
3+
import * as Vuex from "vuex"
4+
import { buildStore } from "./store"
5+
import { RootState } from "./store/index"
6+
import birthday, { birthdayModuleBuilder } from "./store/birthday/birthday"
7+
import auth from "./store/auth/auth"
8+
import { getStoreBuilder } from "../index"
9+
import { StoreBuilder, ModuleBuilder } from "../index"
10+
11+
interface AnonState { age: number }
12+
13+
describe("Create an anon store", () =>
14+
{
15+
let moduleBuilder: ModuleBuilder<AnonState>
16+
beforeEach(() =>
17+
{
18+
moduleBuilder = getStoreBuilder("anon").module("anon", { age: 36 })
19+
})
20+
21+
describe("try to create a getter with anon function", () =>
22+
{
23+
it("should fail", () =>
24+
{
25+
26+
expect(() =>
27+
{
28+
const readApproxDaysAlive = moduleBuilder.read((state: AnonState) => Math.round(state.age * 365.25))
29+
}).to.throw()
30+
})
31+
})
32+
33+
describe("try to create a getter with explicit name", () =>
34+
{
35+
it("should succeed", () =>
36+
{
37+
expect(() =>
38+
{
39+
const readApproxDaysAlive = moduleBuilder.read((state: AnonState) => Math.round(state.age * 365.25), "daysAlive")
40+
}).to.not.throw()
41+
})
42+
})
43+
44+
const daysAliveGetter = (state: AnonState) => Math.round(state.age * 365.25) // <-- named function
45+
describe("try to create a getter with named function", () =>
46+
{
47+
it("should succeed", () =>
48+
{
49+
expect(() =>
50+
{
51+
const readApproxDaysAlive = moduleBuilder.read(daysAliveGetter)
52+
}).to.not.throw()
53+
})
54+
})
55+
})

src/tests/store.spec.ts renamed to src/tests/complete.spec.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { expect } from "chai"
22
import * as Vue from "vue"
33
import * as Vuex from "vuex"
4-
import { createStore } from "./store"
5-
import { RootState } from "./store/index"
4+
import { buildStore, RootState } from "./store/index"
65
import birthday from "./store/birthday/birthday"
76
import auth from "./store/auth/auth"
87

9-
describe("Running an action", () =>
8+
describe("Run an action", () =>
109
{
1110
let store: Vuex.Store<RootState>
1211

1312
beforeEach(() =>
1413
{
1514
Vue.use(Vuex)
16-
store = createStore()
15+
store = buildStore()
1716
store.replaceState({
1817
birthday: {
1918
birthdays: [
@@ -28,11 +27,9 @@ describe("Running an action", () =>
2827
},
2928
auth: { isLoggedIn: false, userID: "" }
3029
})
31-
auth.provideStore(store)
32-
birthday.provideStore(store)
3330
})
3431

35-
describe("remove first 2 birthdays", () =>
32+
describe("that removes first 2 birthdays with delays", () =>
3633
{
3734
it("should show Bertram after removing first two birthdays", async () =>
3835
{

src/tests/main.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect } from "chai"
22
import * as Vue from "vue"
33
import * as Vuex from "vuex"
4-
import { createStore } from "./store"
4+
import { buildStore } from "./store"
55
import { RootState } from "./store/index"
66
import birthday from "./store/birthday/birthday"
77
import auth from "./store/auth/auth"
@@ -11,7 +11,7 @@ let store: Vuex.Store<RootState>
1111
async function test()
1212
{
1313
Vue.use(Vuex)
14-
store = createStore()
14+
store = buildStore()
1515
store.replaceState({
1616
birthday: {
1717
birthdays: [
@@ -26,8 +26,6 @@ async function test()
2626
},
2727
auth: { isLoggedIn: false, userID: "" }
2828
})
29-
auth.provideStore(store)
30-
birthday.provideStore(store)
3129
expect(birthday.oldestName).equal("Erlich")
3230
await birthday.dispatchRemoveFirstAfterDelay(20)
3331
await birthday.dispatchRemoveFirstAfterDelay(20)

src/tests/store/auth/auth.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@ import { ModuleBuilder } from "../../.."
44
import { AuthState } from "./state"
55
import { RootState } from "../index"
66
import { Module } from "vuex"
7+
import { getStoreBuilder } from "../../.."
78

89
const initialState: AuthState = {
910
userID: "b6c8185c6d0af2f5d968",
1011
isLoggedIn: true
1112
}
1213

13-
const a = new ModuleBuilder<AuthState, RootState>("auth", initialState)
14+
const storeBuilder = getStoreBuilder<RootState>()
15+
const moduleBuilder = storeBuilder.module<AuthState>("auth", initialState)
1416

1517
const auth = {
16-
commitSetUserID: a.commit((state, payload: { userID: string }) => state.userID = payload.userID, "setUserID"),
17-
commitSetIsLoggedIn: a.commit((state, payload: { isLoggedIn: boolean }) => state.isLoggedIn = payload.isLoggedIn, "isLoggedIn"),
18-
dispatchLogin: a.dispatch((context) =>
18+
commitSetUserID: moduleBuilder.commit((state, payload: { userID: string }) => state.userID = payload.userID, "setUserID"),
19+
commitSetIsLoggedIn: moduleBuilder.commit((state, payload: { isLoggedIn: boolean }) => state.isLoggedIn = payload.isLoggedIn, "isLoggedIn"),
20+
dispatchLogin: moduleBuilder.dispatch((context) =>
1921
{
2022
return
21-
}, "login"),
22-
provideStore: a.provideStore()
23+
}, "login")
2324
}
2425

25-
export default auth
26-
export const authModule: Module<AuthState, RootState> = a.toVuexModule()
26+
export default auth

0 commit comments

Comments
 (0)