Skip to content

Instantly share code, notes, and snippets.

@f-space
Last active March 18, 2019 19:48
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save f-space/4f42e1f4b3542065ef84c91d97859a0e to your computer and use it in GitHub Desktop.
Save f-space/4f42e1f4b3542065ef84c91d97859a0e to your computer and use it in GitHub Desktop.
Decorators for class-style vuex modules.
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;
}
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