diff --git a/package-lock.json b/package-lock.json index c4d0afb..0083175 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2881,6 +2881,12 @@ "kind-of": "3.2.2" } }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, "is-posix-bracket": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", @@ -2893,6 +2899,12 @@ "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", "dev": true }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -3991,6 +4003,27 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, + "quibble": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/quibble/-/quibble-0.5.3.tgz", + "integrity": "sha512-HL+gtKkDOo1HlxDpWaBd2xbkVg3sQeP0mS39kdF1CzkdNcY0bOVxzjGOs35oEjbDTbL8DtgP24UGgrd0cr9x8w==", + "dev": true, + "requires": { + "lodash": "4.17.4", + "resolve": "1.5.0" + }, + "dependencies": { + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + } + } + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", @@ -4593,6 +4626,16 @@ "type-name": "2.0.2" } }, + "stringify-object-es5": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/stringify-object-es5/-/stringify-object-es5-2.5.0.tgz", + "integrity": "sha1-BXw8mpChJzObudFwSikLt70KHsU=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0", + "is-regexp": "1.0.0" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -4646,6 +4689,18 @@ "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=", "dev": true }, + "testdouble": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/testdouble/-/testdouble-3.3.2.tgz", + "integrity": "sha512-RUsGZt/tu8A+OkZJa23qnmaEcbAKfSkV0/YVDAg+rEIJg03QUZcVBhUER0JOoHHB/y8S5o3wmieQh8e6bAJk+w==", + "dev": true, + "requires": { + "es6-map": "0.1.5", + "lodash": "4.17.4", + "quibble": "0.5.3", + "stringify-object-es5": "2.5.0" + } + }, "testem": { "version": "1.18.4", "resolved": "https://registry.npmjs.org/testem/-/testem-1.18.4.tgz", @@ -4824,9 +4879,9 @@ "dev": true }, "typescript": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", - "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", + "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", "dev": true }, "uglify-js": { @@ -4996,21 +5051,19 @@ } }, "vue": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.5.2.tgz", - "integrity": "sha512-Au9rf8fPkBulFHfZ406UaQDd1jH9fqGRIM+0IHilrXnJ/0TeeMH4SBkNxWf2dGevl2S3aVeu0E/WklEv0/msag==", + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.5.13.tgz", + "integrity": "sha512-3D+lY7HTkKbtswDM4BBHgqyq+qo8IAEE8lz8va1dz3LLmttjgo0FxairO4r1iN2OBqk8o1FyL4hvzzTFEdQSEw==", "dev": true }, "vue-class-component": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-6.0.0.tgz", - "integrity": "sha512-3XS48fRq8NoTg/SgGOoHc50xiwgIkaee3/eyFcHl5BlzU5EW4phN3q5yh8aLdJ3vzcW1jxdiEyI6davLq+VJ0w==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-6.1.2.tgz", + "integrity": "sha512-DF0PIhpBDiQdr+Rofd3HQ79N722hMVPQ8PDMt9vCD4Q7vCnOE3Dgn75ZuvRPYrNvkJtn26HWgwgR83XcJGmxGA==", "dev": true }, "vuex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.0.0.tgz", - "integrity": "sha512-/QPzbRtnj39bUYTRWfFScZ7RWXBxlaQxqfAWuHTKxJoxrBHPANaQNUhY4aUjBlLbAfOOJTOgx17gCcyX8h2AhQ==", + "version": "github:ktsn/vuex#8b6a6f919a553f2342d6f839491c0e26be7449d8", "dev": true }, "watchpack": { diff --git a/package.json b/package.json index f42e58a..723dd61 100644 --- a/package.json +++ b/package.json @@ -46,15 +46,16 @@ "rollup": "^0.50.0", "rollup-plugin-replace": "^2.0.0", "sinon": "^4.0.1", + "testdouble": "^3.3.2", "testem": "^1.18.4", "ts-loader": "^3.0.2", "tslint": "^5.7.0", "tslint-config-ktsn": "^2.1.0", "typescript": "^2.5.3", "uglify-js": "^3.1.4", - "vue": "^2.5.2", - "vue-class-component": "^6.0.0", - "vuex": "^3.0.0", + "vue": "^2.5.13", + "vue-class-component": "^6.1.2", + "vuex": "github:ktsn/vuex#8b6a6f9", "webpack": "^3.8.1", "webpack-espower-loader": "^1.0.2" }, diff --git a/scripts/webpack.config.test.js b/scripts/webpack.config.test.js index f574af4..f3d4bff 100644 --- a/scripts/webpack.config.test.js +++ b/scripts/webpack.config.test.js @@ -2,7 +2,8 @@ const path = require('path') const glob = require('glob') module.exports = { - entry: ['es6-promise/auto'].concat(glob.sync(path.resolve(__dirname, '../test/**/*.ts'))), + entry: ['es6-promise/auto', path.resolve(__dirname, '../test/setup.ts')] + .concat(glob.sync(path.resolve(__dirname, '../test/**/*.spec.ts'))), output: { path: path.resolve(__dirname, '../.tmp'), filename: 'test.js' diff --git a/src/bind-store.ts b/src/bind-store.ts new file mode 100644 index 0000000..479e647 --- /dev/null +++ b/src/bind-store.ts @@ -0,0 +1,107 @@ +import Vue, { VueConstructor, ComponentOptions } from 'vue' +import { merge, mapValues } from './utils' + +export interface Class { + new (...args: any[]): Instance +} + +export type MutationMethod

= (payload: P) => void +export type ActionMethod

= (payload: P) => Promise + +export interface StoreBinder { + create(): Class & typeof Vue + + state(map: Key[]): StoreBinder + state>(map: Map): StoreBinder + + getters(map: Key[]): StoreBinder + getters>(map: Map): StoreBinder + + mutations(map: Key[]): StoreBinder }, State, Getters, Mutations, Actions> + mutations>(map: Map): StoreBinder }, State, Getters, Mutations, Actions> + + actions(map: Key[]): StoreBinder }, State, Getters, Mutations, Actions> + actions>(map: Map): StoreBinder }, State, Getters, Mutations, Actions> +} + +export function bindStore(namespace?: string): StoreBinder { + return createBinder({}) +} + +function createBinder(options: ComponentOptions): StoreBinder { + return { + state(map: string[] | Record) { + const computed = merge( + options.computed || {}, + mapPoly(map, value => makeComputed(value, 'state')) + ) + + const newOptions = merge(options, { computed }) + + return createBinder(newOptions) + }, + + getters(map: string[] | Record) { + const computed = merge( + options.computed || {}, + mapPoly(map, value => makeComputed(value, 'getters')) + ) + + const newOptions = merge(options, { computed }) + + return createBinder(newOptions) + }, + + mutations(map: string[] | Record) { + const methods = merge( + options.methods || {}, + mapPoly(map, value => makeMethod(value, 'commit')) + ) + + const newOptions = merge(options, { methods }) + + return createBinder(newOptions) + }, + + actions(map: string[] | Record) { + const methods = merge( + options.methods || {}, + mapPoly(map, value => makeMethod(value, 'dispatch')) + ) + + const newOptions = merge(options, { methods }) + + return createBinder(newOptions) + }, + + create() { + return Vue.extend(options) + } + } +} + +function mapPoly( + map: string[] | Record, + fn: (value: string, key: string) => R +): Record { + if (Array.isArray(map)) { + map = map.reduce>((acc, value) => { + acc[value] = value + return acc + }, {}) + } + + return mapValues(map, fn) +} + +function makeComputed(key: string, type: 'state' | 'getters'): () => any { + return function boundComputed (this: Vue): any { + return this.$store[type][key] + } +} + +function makeMethod(key: string, type: 'dispatch' | 'commit'): (payload: any) => any { + return function boundMethod (this: Vue, payload: any): any { + return (this.$store[type] as Function)(key, payload) + } +} diff --git a/src/bindings.ts b/src/decorators.ts similarity index 90% rename from src/bindings.ts rename to src/decorators.ts index ac6b75f..533a13d 100644 --- a/src/bindings.ts +++ b/src/decorators.ts @@ -6,13 +6,16 @@ import { mapActions, mapMutations } from 'vuex' +import { merge } from './utils' export type VuexDecorator = (proto: V, key: string) => void export type StateTransformer = (state: any, getters: any) => any -export type MapHelper = typeof mapState | typeof mapGetters - | typeof mapActions | typeof mapMutations +export interface MapHelper { + (map: string[] | Record): Record + (namespace: string, map: string[] | Record): Record +} export interface BindingOptions { namespace?: string @@ -105,13 +108,3 @@ function extractNamespace (options: BindingOptions | undefined): string | undefi return n } - -function merge (a: T, b: U): T & U { - const res: any = {} - ;[a, b].forEach((obj: any) => { - Object.keys(obj).forEach(key => { - res[key] = obj[key] - }) - }) - return res -} diff --git a/src/index.ts b/src/index.ts index 410c20e..b10b0c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,4 +4,4 @@ export { Action, Mutation, namespace -} from './bindings' +} from './decorators' diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..59b022a --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,20 @@ +export function merge (a: T, b: U): T & U { + const res: any = {} + ;[a, b].forEach((obj: any) => { + Object.keys(obj).forEach(key => { + res[key] = obj[key] + }) + }) + return res +} + +export function mapValues( + obj: Record, + fn: (value: T, key: string) => R +): Record { + const res: Record = {} + Object.keys(obj).forEach(key => { + res[key] = fn(obj[key], key) + }) + return res +} diff --git a/test/bind-store.spec.ts b/test/bind-store.spec.ts new file mode 100644 index 0000000..3ed7a2c --- /dev/null +++ b/test/bind-store.spec.ts @@ -0,0 +1,179 @@ +import * as assert from 'power-assert' +import * as td from 'testdouble' +import Component from 'vue-class-component' +import { Store, DefineModule } from 'vuex' +import { bindStore } from '../src/bind-store' + +interface State { + count: number +} + +interface Getters { + double: number +} + +interface Mutations { + increment: number +} + +interface Actions { + incrementAsync: { + delay: number + amount: number + } +} + +const counter: DefineModule = { + state: () => ({ + count: 0 + }), + getters: { + double: state => state.count * 2 + }, + mutations: { + increment: td.function() as any + }, + actions: { + incrementAsync: td.function() as any + } +} + +describe('bindStore', () => { + let store: Store + beforeEach(() => { + // @ts-ignore + store = new Store(counter) + }) + + it('binds states', () => { + const Super = bindStore() + .state(['count']) + .create() + + @Component + class Test extends Super {} + + const vm = new Test({ store }) + assert(vm.count === 0) + store.state.count++ + assert(vm.count === 1) + }) + + it('binds state by using object mapper', () => { + const Super = bindStore() + .state({ + value: 'count' + }) + .create() + + @Component + class Test extends Super {} + + const vm = new Test({ store }) + assert(vm.value === 0) + store.state.count++ + assert(vm.value === 1) + }) + + it('binds getters', () => { + const Super = bindStore() + .getters(['double']) + .create() + + @Component + class Test extends Super {} + + const vm = new Test({ store }) + assert(vm.double === 0) + store.state.count++ + assert(vm.double === 2) + }) + + it('binds getters by using object mapper', () => { + const Super = bindStore() + .getters({ + multiply2: 'double' + }) + .create() + + @Component + class Test extends Super {} + + const vm = new Test({ store }) + assert(vm.multiply2 === 0) + store.state.count++ + assert(vm.multiply2 === 2) + }) + + it('binds mutations', () => { + const Super = bindStore() + .mutations(['increment']) + .create() + + @Component + class Test extends Super {} + + const vm = new Test({ store }) + vm.increment(123) + td.verify(counter.mutations!.increment( + td.matchers.anything(), + 123 + )) + }) + + it('binds mutations by using object mapper', () => { + const Super = bindStore() + .mutations({ + plus: 'increment' + }) + .create() + + @Component + class Test extends Super {} + + const vm = new Test({ store }) + vm.plus(123) + td.verify(counter.mutations!.increment( + td.matchers.anything(), + 123 + )) + }) + + it('binds actions', () => { + const Super = bindStore() + .actions(['incrementAsync']) + .create() + + @Component + class Test extends Super {} + + const vm = new Test({ store }) + vm.incrementAsync({ delay: 100, amount: 42 }) + td.verify(counter.actions!.incrementAsync( + td.matchers.anything(), + { delay: 100, amount: 42 } + ), { + ignoreExtraArgs: true + }) + }) + + it('binds actions by using object mapper', () => { + const Super = bindStore() + .actions({ + delayedPlus: 'incrementAsync' + }) + .create() + + @Component + class Test extends Super {} + + const vm = new Test({ store }) + vm.delayedPlus({ delay: 100, amount: 42 }) + td.verify(counter.actions!.incrementAsync( + td.matchers.anything(), + { delay: 100, amount: 42 } + ), { + ignoreExtraArgs: true + }) + }) +}) diff --git a/test/bindings.ts b/test/decorators.spec.ts similarity index 99% rename from test/bindings.ts rename to test/decorators.spec.ts index f5f507f..28f7a14 100644 --- a/test/bindings.ts +++ b/test/decorators.spec.ts @@ -9,11 +9,9 @@ import { Action, Mutation, namespace -} from '../src/bindings' +} from '../src/decorators' describe('binding helpers', () => { - Vue.use(Vuex) - it('State: type', () => { const store = new Vuex.Store({ state: { value: 1 } diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..bc78d14 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,7 @@ +import Vue from 'vue' +import Vuex from 'vuex' + +Vue.config.productionTip = false +Vue.config.devtools = false + +Vue.use(Vuex) diff --git a/tsconfig.base.json b/tsconfig.base.json index 846fc1d..7dd3f22 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -8,10 +8,6 @@ "dom", "es2015" ], - "allowSyntheticDefaultImports": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "strictNullChecks": true + "strict": true } }