diff --git a/README.md b/README.md index b161619..9cbbf28 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,77 @@ export class MyComp extends Vue { } ``` +### Curried Getters + +You might have a getter that returns a curried function in order to pass in parameters to your getter. E.g.: + +```ts +const store = { + state: { + values: [ + { id: 1, value: "value 1" }, + { id: 2, value: "value 2" } + ] + }, + getters: { + byId: state => id => state.values.filter(v => v.id === id)[0].value + } +} + +``` + +You could bind this getter to a function on your component class: + +```ts +import Vue from 'vue' +import Component from 'vue-class-component' +import { + Getter +} from 'vuex-class' + + +@Component +export class MyComp extends Vue { + + @Getter("byId") byId: (id: number) => string; +} +``` + +But you might have a static value that you want to bind to. In which case you can pass this to the args property of the binding options object: + +```ts +import Vue from 'vue' +import Component from 'vue-class-component' +import { + Getter +} from 'vuex-class' + + +@Component +export class MyComp extends Vue { + + @Getter("byId", { args: [1] }) value: string; +} +``` + +You might want to encapsulate this into your own decorator so you can strongly type the arguments: + +```ts +import Vue from 'vue' +import Component from 'vue-class-component' +import { + Getter +} from 'vuex-class' + +const ByIdGetter = (id: number) => @Getter("byId", { args: [id] }) + +@Component +export class MyComp extends Vue { + + @ByIdGetter(1) byId: (id: number) => string; +} +``` + ## Issue Reporting Guideline ### Questions diff --git a/package-lock.json b/package-lock.json index 2d58ec2..76ae8fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4196,7 +4196,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4217,12 +4218,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4237,17 +4240,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4364,7 +4370,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4376,6 +4383,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4390,6 +4398,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4397,12 +4406,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4421,6 +4432,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4501,7 +4513,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4513,6 +4526,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4598,7 +4612,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4634,6 +4649,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4653,6 +4669,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4696,12 +4713,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/src/bindings.ts b/src/bindings.ts index f0c9231..93a81e5 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -4,7 +4,8 @@ import { mapState, mapGetters, mapActions, - mapMutations + mapMutations, + Dictionary } from 'vuex' export type VuexDecorator = (proto: V, key: string) => void @@ -15,7 +16,8 @@ export type MapHelper = typeof mapState | typeof mapGetters | typeof mapActions | typeof mapMutations export interface BindingOptions { - namespace?: string + namespace?: string, + args?: any[] } export interface BindingHelper { @@ -36,7 +38,35 @@ export interface BindingHelpers { export const State = createBindingHelper('computed', mapState) as StateBindingHelper -export const Getter = createBindingHelper('computed', mapGetters) + +const curriedMapGetters = (args: any[]) => { + + function map(map: string[]): Dictionary + function map(map: Dictionary): Dictionary + function map(namespace: string, map: string[]): Dictionary + function map(namespace: string, map: Dictionary): Dictionary + function map(...mapArgs: any[]): Dictionary { + + + const mappedGetters = (mapGetters as any)(...mapArgs) + + + const entries: ([string, any])[] = Object.keys(mappedGetters).map<[string, any]>(k => ([k, mappedGetters[k]])) + + return entries.reduce( + (acc, [getter, fn]) => ({ + ...acc, + [getter]: (state: any) => + fn.call(state)(...(Array.isArray(args) ? args : [args])) + }), + {} + ) + } + + return map +} + +export const Getter = createBindingHelper('computed', mapGetters, curriedMapGetters) export const Action = createBindingHelper('methods', mapActions) @@ -77,7 +107,7 @@ export function namespace ( return { State: createNamespacedHelper(State as any), - Getter: createNamespacedHelper(Getter as any), + Getter: createNamespacedHelper(createBindingHelper('computed', mapGetters, curriedMapGetters) as any), Mutation: createNamespacedHelper(Mutation as any), Action: createNamespacedHelper(Action as any) } @@ -85,9 +115,10 @@ export function namespace ( function createBindingHelper ( bindTo: 'computed' | 'methods', - mapFn: MapHelper + mapFn: MapHelper, + curriedMapFn?: (...args: any[]) => MapHelper ): BindingHelper { - function makeDecorator (map: any, namespace: string | undefined) { + function makeDecorator (map: any, namespace: string | undefined, args: any[] = []) { return createDecorator((componentOptions, key) => { if (!componentOptions[bindTo]) { componentOptions[bindTo] = {} @@ -95,6 +126,10 @@ function createBindingHelper ( const mapObject = { [key]: map } + if (args.length > 0 && curriedMapFn) { + mapFn = curriedMapFn(args) + } + componentOptions[bindTo]![key] = namespace !== undefined ? mapFn(namespace, mapObject)[key] : mapFn(mapObject)[key] @@ -111,8 +146,9 @@ function createBindingHelper ( } const namespace = extractNamespace(b) + const args = extractArgs(b) const type = a - return makeDecorator(type, namespace) + return makeDecorator(type, namespace, args) } return helper @@ -132,6 +168,16 @@ function extractNamespace (options: BindingOptions | undefined): string | undefi return n } +function extractArgs(options: BindingOptions | undefined): any[] { + const args = options && options.args + + if (args != null) { + return args + } + + return [] +} + function merge (a: T, b: U): T & U { const res: any = {} ;[a, b].forEach((obj: any) => { diff --git a/test/bindings.ts b/test/bindings.ts index 57c7085..4db0a00 100644 --- a/test/bindings.ts +++ b/test/bindings.ts @@ -102,6 +102,8 @@ describe('binding helpers', () => { const c = new MyComp({ store }) assert(c.bar === 2) + store.state.value = 2; + assert(c.bar === 3) }) it('Getter: implicit getter type', () => { @@ -121,6 +123,59 @@ describe('binding helpers', () => { assert(c.foo === 2) }) + it('Getter: with arguments', () => { + const store = new Vuex.Store({ + state: { + values: [ + { id: 1, value: "a" }, + { id: 2, value: "b" } + ] + }, + getters: { + byId: state => (id: number) => { + return state.values.filter(v => v.id === id)[0].value + } + } + }) + + @Component + class MyComp extends Vue { + @Getter("byId", { args: [1] }) foo: string; + } + + const c = new MyComp({ store }) + assert(c.foo === "a"); + + store.state.values[0].value = "c" + assert(c.foo === "c"); + }) + + it('Getter: with arguments encapsulated', () => { + const store = new Vuex.Store({ + state: { + values: [ + { id: 1, value: "a" }, + { id: 2, value: "b" } + ] + }, + getters: { + byId: state => (id: number) => { + return state.values.filter(v => v.id === id)[0].value + } + } + }) + + const ById = (id: number) => Getter("byId", { args: [id] }); + + @Component + class MyComp extends Vue { + @ById(2) foo: string; + } + + const c = new MyComp({ store }) + assert(c.foo === "b"); + }) + it('Getter: namespace', () => { const store = new Vuex.Store({ modules: { @@ -149,6 +204,70 @@ describe('binding helpers', () => { assert(c.bar === 2) }) + it('Getter: namespace with parameters', () => { + const store = new Vuex.Store({ + modules: { + foo: { + namespaced: true, + state: { + values: [ + { id: 1, value: "a" }, + { id: 2, value: "b" } + ] + }, + getters: { + byId: (state: { values: ({ id: number, value: string})[] }) => (id: number) => + state.values.filter(v => v.id === id)[0].value + } + } + } + }) + + const foo = namespace('foo') + + @Component + class MyComp extends Vue { + @foo.Getter('byId', { args: [1] }) + baz: string + } + + const c = new MyComp({ store }) + assert(c.baz === "a") + }) + + it('Getter: namespace with parameters encapsulated', () => { + const store = new Vuex.Store({ + modules: { + foo: { + namespaced: true, + state: { + values: [ + { id: 1, value: "a" }, + { id: 2, value: "b" } + ] + }, + getters: { + byId: (state: { values: ({ id: number, value: string})[] }) => (id: number) => + state.values.filter(v => v.id === id)[0].value + } + } + } + }) + + const foo = namespace('foo') + + const ById = (id: number) => foo.Getter('byId', { args: [id] }) + + @Component + class MyComp extends Vue { + @ById(2) + baz: string + } + + const c = new MyComp({ store }) + assert(c.baz === "b") + }) + it('Action: type', () => { const spy = sinon.spy() @@ -169,7 +288,7 @@ describe('binding helpers', () => { assert.deepStrictEqual(spy.getCall(0).args[1], { value: 1 }) }) - it('Action: implicity action type', () => { + it('Action: implicit action type', () => { const spy = sinon.spy() const store = new Vuex.Store({