Last active
March 18, 2019 19:48
-
-
Save f-space/4f42e1f4b3542065ef84c91d97859a0e to your computer and use it in GitHub Desktop.
Decorators for class-style vuex modules.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as Vuex from 'vuex'; | |
import 'reflect-metadata'; | |
export const TypeSymbol = Symbol('type'); | |
export interface IModule<R = any> { | |
readonly $state: any; | |
readonly $getters: any; | |
readonly $commit: Vuex.Commit; | |
readonly $dispatch: Vuex.Dispatch; | |
readonly $root: R; | |
} | |
interface DecoratedClass extends Function { | |
namespaced?: boolean; | |
__members__?: { | |
getters?: { [key: string]: Function }; | |
mutations?: { [key: string]: Function }; | |
actions?: { [key: string]: Function }; | |
modules?: { [key: string]: DecoratedClass }; | |
} | |
} | |
interface Context { | |
readonly state: any; | |
readonly getters: any; | |
readonly rootState: any; | |
readonly rootGetters: any; | |
readonly commit: Vuex.Commit; | |
readonly dispatch: Vuex.Dispatch; | |
readonly path: string; | |
readonly writable: boolean; | |
} | |
class GetterContext implements Context { | |
constructor(readonly state: any, readonly getters: any, readonly rootState: any, readonly rootGetters: any) { } | |
public get commit(): never { throw new Error(getUnusableErrorMessage("getters", "commit")); } | |
public get dispatch(): never { throw new Error(getUnusableErrorMessage("getters", "dispatch")); } | |
public get path(): string { return ""; } | |
public get writable(): false { return false; } | |
} | |
class MutationContext implements Context { | |
constructor(readonly state: any) { } | |
public get getters(): never { throw new Error(getUnusableErrorMessage("mutations", "getters")) } | |
public get rootState(): never { throw new Error(getUnusableErrorMessage("mutations", "rootState")) } | |
public get rootGetters(): never { throw new Error(getUnusableErrorMessage("mutations", "rootGetters")) } | |
public get commit(): never { throw new Error(getUnusableErrorMessage("mutations", "commit")) } | |
public get dispatch(): never { throw new Error(getUnusableErrorMessage("mutations", "displatch")) } | |
public get path(): string { return ""; } | |
public get writable(): true { return true; } | |
} | |
class ActionContext implements Context { | |
constructor(readonly context: Vuex.ActionContext<any, any>) { } | |
public get state(): any { return this.context.state; } | |
public get getters(): any { return this.context.getters; } | |
public get rootState(): any { return this.context.rootState; } | |
public get rootGetters(): any { return this.context.rootGetters; } | |
public get commit(): Vuex.Commit { return this.context.commit; } | |
public get dispatch(): Vuex.Dispatch { return this.context.dispatch; } | |
public get path(): string { return ""; } | |
public get writable(): false { return false; } | |
} | |
class RootContext implements Context { | |
constructor(readonly context: Context) { } | |
public get state(): any { return this.context.rootState; } | |
public get getters(): any { return this.context.rootGetters; } | |
public get rootState(): any { return this.context.rootState; } | |
public get rootGetters(): any { return this.context.rootGetters; } | |
public get commit(): Vuex.Commit { return (type: string, payload: any) => this.context.commit(type, payload, { root: true }); } | |
public get dispatch(): Vuex.Dispatch { return (type: string, payload: any) => this.context.dispatch(type, payload, { root: true }); } | |
public get path(): string { return ""; } | |
public get writable(): boolean { return this.context.writable; } | |
} | |
class ModuleContext implements Context { | |
constructor(readonly parent: Context, readonly state: any, readonly path: string) { } | |
public get getters(): any { return this.parent.getters; } | |
public get rootState(): any { return this.parent.rootState; } | |
public get rootGetters(): any { return this.parent.rootGetters; } | |
public get commit(): Vuex.Commit { return this.parent.commit; } | |
public get dispatch(): Vuex.Dispatch { return this.parent.dispatch; } | |
public get writable(): boolean { return this.parent.writable; } | |
} | |
function getUnusableErrorMessage(context: string, property: string): string { | |
return `Unable to use '${property}' in ${context}.`; | |
} | |
export function Module(options: Vuex.StoreOptions<any> | Vuex.Module<any, any>): <T>(target: T) => T; | |
export function Module<T>(target: T): T; | |
export function Module(): any { | |
if (typeof arguments[0] === 'function') { | |
return createModule(arguments[0]); | |
} else { | |
const options = arguments[0]; | |
return function (target: any) { | |
return createModule(target, options); | |
} | |
} | |
} | |
export function Getter(target: any, key: string, descriptor: PropertyDescriptor): void { | |
if (typeof descriptor.get !== 'function') throw new Error(`${key} is not a getter.`); | |
setMember('getters', target, key, descriptor.get); | |
} | |
export function Mutation(target: any, key: string, descriptor: PropertyDescriptor): void { | |
if (typeof descriptor.value !== 'function') throw new Error(`${key} is not a method.`); | |
setMember('mutations', target, key, descriptor.value); | |
} | |
export function Action(target: any, key: string, descriptor: PropertyDescriptor): void { | |
if (typeof descriptor.value !== 'function') throw new Error(`${key} is not a method.`); | |
setMember('actions', target, key, descriptor.value); | |
} | |
export function Child(child: object): (target: any, key: string) => void; | |
export function Child(target: any, key: string): void; | |
export function Child(): any { | |
if (arguments.length === 1) { | |
const child = arguments[0]; | |
return function (target: any, key: string) { | |
setModule(target, key, child); | |
} | |
} else { | |
const target = arguments[0] as any; | |
const key = arguments[1] as string; | |
const child = Reflect.getMetadata('design:type', target, key); | |
setModule(target, key, child); | |
} | |
function setModule(target: any, key: string, child: any) { | |
if (typeof child !== 'function' && typeof child !== 'object') throw new Error(`${child} is not a module definition.`); | |
setMember('modules', target, key, child); | |
} | |
} | |
function setMember(type: 'getters' | 'mutations' | 'actions' | 'modules', target: any, key: string, value: any): void { | |
if (typeof target.constructor !== 'function') throw new Error(`${target} has no constructor function.`); | |
const Module = target.constructor as DecoratedClass; | |
const members = Module.__members__ || (Module.__members__ = {}); | |
const entries = members[type] || (members[type] = Object.create(null)); | |
entries[key] = value; | |
} | |
function createModule(Module: Vuex.Module<any, any> & DecoratedClass, options?: any): any { | |
Object.assign(Module, options); | |
const master = Object.assign({ [TypeSymbol]: Module }, Reflect.construct(Module, []), Module.state); | |
Module.state = () => Object.assign({}, master); | |
if (Module.__members__) { | |
const members = Module.__members__; | |
if (members.getters) { | |
const getters = Object.create(null); | |
for (const key in members.getters) { | |
const original = members.getters[key]; | |
getters[key] = function (state: any, getters: any, rootState: any, rootGetters: any): any { | |
return original.call(getProxy(new GetterContext(state, getters, rootState, rootGetters))); | |
} | |
} | |
Module.getters = Object.assign(getters, Module.getters); | |
} | |
if (members.mutations) { | |
const mutations = Object.create(null); | |
for (const key in members.mutations) { | |
const original = members.mutations[key]; | |
mutations[key] = function (state: any, payload?: any[]): any { | |
return original.apply(getProxy(new MutationContext(state)), payload); | |
} | |
} | |
Module.mutations = Object.assign(mutations, Module.mutations); | |
} | |
if (members.actions) { | |
const actions = Object.create(null); | |
for (const key in members.actions) { | |
const original = members.actions[key]; | |
actions[key] = function (context: any, payload?: any[]): any { | |
return original.apply(getProxy(new ActionContext(context)), payload); | |
} | |
} | |
Module.actions = Object.assign(actions, Module.actions); | |
} | |
if (members.modules) { | |
Module.modules = Object.assign(Object.create(null), members.modules, Module.modules); | |
} | |
} | |
return Module; | |
} | |
const handlers = new WeakMap<DecoratedClass, ProxyHandler<Context>>(); | |
let altHandler: ProxyHandler<Context>; | |
type PropertyMap = { [key: string]: (context: Context, key: string) => any }; | |
const basicProps: PropertyMap = { | |
$state(context) { return context.state; }, | |
$getters(context) { return context.getters; }, | |
$commit(context) { return context.commit; }, | |
$dispatch(context) { return context.dispatch; }, | |
$root(context) { return getProxy(new RootContext(context)); }, | |
}; | |
function getProxy(context: Context): any { | |
return new Proxy(context, getProxyHandler(context.state[TypeSymbol])); | |
} | |
function getProxyHandler(Module?: DecoratedClass): ProxyHandler<Context> { | |
if (Module) { | |
let handler = handlers.get(Module); | |
if (!handler) handlers.set(Module, handler = createProxyHandler(Module)); | |
return handler; | |
} | |
return altHandler || (altHandler = createProxyHandler()); | |
} | |
function createProxyHandler(Module?: DecoratedClass): ProxyHandler<Context> { | |
return { get: createProxyGetHandler(Module), set: setHandler }; | |
} | |
function createProxyGetHandler(Module?: DecoratedClass): ProxyHandler<Context>['get'] { | |
const props = Object.assign(Object.create(null) as {}, basicProps); | |
const members = Module && Module.__members__; | |
if (members) { | |
for (const key in members.getters || {}) props[key] = getGetter; | |
for (const key in members.mutations || {}) props[key] = getMutation; | |
for (const key in members.actions || {}) props[key] = getAction; | |
for (const key in members.modules || {}) props[key] = getModule; | |
} | |
const prototype = (Module && Module.prototype) || Object.create(null); | |
return function (target, prop) { | |
return ( | |
prop in props ? props[prop](target, prop as string) : | |
prop in prototype ? prototype[prop] : target.state[prop] | |
); | |
}; | |
} | |
function getGetter(context: Context, key: string): any { | |
return context.getters[context.path + key]; | |
} | |
function getMutation(context: Context, key: string): any { | |
return (...args: any[]) => context.commit.call(null, context.path + key, args); | |
} | |
function getAction(context: Context, key: string): any { | |
return (...args: any[]) => context.dispatch.call(null, context.path + key, args); | |
} | |
function getModule(context: Context, key: string): any { | |
const state = context.state[key]; | |
const type = state[TypeSymbol]; | |
const path = type && type.namespaced ? `${context.path}${key}/` : context.path; | |
return getProxy(new ModuleContext(context, state, path)); | |
} | |
function setHandler(target: Context, prop: PropertyKey, value: any): boolean { | |
return target.writable ? Reflect.set(target.state, prop, value) : false; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Vue from 'vue'; | |
import Vuex from 'vuex'; | |
import { Module, Getter, Mutation, Action, Child } from "./vuex-class-module"; | |
Vue.use(Vuex); | |
@Module({ namespaced: true }) | |
class Foo { | |
value: number = 0; | |
@Getter | |
get square(): number { return this.value * this.value; } | |
@Mutation | |
set(value: number) { | |
this.value = value; | |
} | |
@Action | |
async add(...values: number[]) { | |
const sum = await this.sum(values); | |
this.set(this.value + sum); | |
} | |
sum(values: number[]) { | |
return new Promise<number>(resolve => resolve(values.reduce((sum, x) => sum + x, 0))); | |
} | |
} | |
/* | |
* To access the root, import IModule and uncomment the following line, then access it with $root. | |
* $state, $getters, $commit, and $dispatch also get to be accessible. | |
*/ | |
// interface Foo extends IModule<Bar> { } | |
@Module | |
class Bar { | |
@Getter | |
get cubic(): number { return this.foo.value * this.foo.square; } | |
@Action | |
addRange(n: number) { | |
function* range(n: number) { | |
for (let i = 0; i < n; i++) yield i; | |
} | |
return this.foo.add(...range(n)); | |
} | |
@Child(Foo) | |
foo: Foo; | |
} | |
const store = new Vuex.Store<Bar>(Bar as any); | |
store.dispatch("addRange", [10]).then(() => { | |
console.log(store.state.foo.value); // 45 | |
store.dispatch("foo/add", [-45, 8]).then(() => { | |
console.log(store.state.foo.value); // 8 | |
console.log(store.getters["foo/square"]); // 64 | |
console.log(store.getters["cubic"]); // 512 | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment