Skip to content

Commit a26d2da

Browse files
authored
feat: add createMapper convenience utility (#264)
1 parent 0572a30 commit a26d2da

File tree

12 files changed

+384
-4
lines changed

12 files changed

+384
-4
lines changed

Diff for: docs/pages/en/2.accessor/1..accessor-introduction.md renamed to docs/pages/en/2.accessor/1.accessor-introduction.md

+40-2
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export default defineComponent({
8686
const counter = computed(() => root.$accessor.counter)
8787
8888
return {
89-
counter
89+
counter,
9090
}
9191
},
9292
})
@@ -107,7 +107,8 @@ you may want to explicitly define accessor types like in the following example:
107107
import { wrapProperty } from '@nuxtjs/composition-api'
108108
import { accessorType } from '~/store'
109109
110-
export const useAccessor = (): typeof accessorType => (wrapProperty('$accessor', false))()
110+
export const useAccessor = (): typeof accessorType =>
111+
wrapProperty('$accessor', false)()
111112
```
112113

113114
### Middleware
@@ -120,3 +121,40 @@ export default ({ redirect, app: { $accessor } }: Context) => {
120121
if ($accessor.email) return redirect('/')
121122
}
122123
```
124+
125+
## Mapping properties into your component
126+
127+
`typed-vuex` also exports a convenience helper function to allow you to easily map the accessor into your component.
128+
129+
```ts{}[components/sampleComponent.vue]
130+
import Vue from 'vue'
131+
132+
// Nuxt
133+
import { accessorType } from '~/store'
134+
const mapper = createMapper(accessorType)
135+
// Vue
136+
import { accessor } from './src/store'
137+
const mapper = createMapper(accessor)
138+
139+
export default Vue.extend({
140+
computed: {
141+
// Direct mapping to a property
142+
...mapper('name'),
143+
// or array of properties
144+
...mapper(['name', 'email']),
145+
// or submodules
146+
...mapper('submodule', ['name', 'email']),
147+
},
148+
methods: {
149+
// All your methods should be included in the methods object,
150+
// whether they are mutations, actions (or even a method returned
151+
// by a getter or stored in state)
152+
...mapper('resetName'),
153+
resetEmail() {
154+
// You can access directly off your component instance
155+
console.log(this.name, this.email)
156+
this.resetName()
157+
},
158+
},
159+
})
160+
```

Diff for: packages/nuxt-typed-vuex/test/fixture/pages/index.vue

+9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
<script>
1414
import Vue from 'vue'
15+
import { mapper } from '~/store'
1516
1617
export default Vue.extend({
1718
asyncData() {
@@ -24,12 +25,20 @@ export default Vue.extend({
2425
this.int = setInterval(() => {
2526
this.date = Date.now()
2627
}, 1000)
28+
this.setEmail('[email protected]')
29+
console.log(this.fullEmail)
30+
this.setFirstName('John')
2731
},
2832
computed: {
33+
...mapper('fullEmail'),
2934
computedDate() {
3035
return new Date(this.date)
3136
},
3237
},
38+
methods: {
39+
...mapper(['setEmail']),
40+
...mapper('submodule', ['setFirstName']),
41+
},
3342
beforeDestroy() {
3443
clearInterval(this.int)
3544
},

Diff for: packages/nuxt-typed-vuex/test/fixture/store/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getAccessorType,
55
mutationTree,
66
actionTree,
7+
createMapper,
78
} from '../../../../typed-vuex/src'
89

910
import * as submodule from './submodule'
@@ -56,3 +57,4 @@ export const pattern = {
5657
export const storeType = getStoreType(pattern)
5758

5859
export const accessorType = getAccessorType(pattern)
60+
export const mapper = createMapper(accessorType)

Diff for: packages/typed-vuex/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"test:types": "yarn tsd"
3434
},
3535
"devDependencies": {
36+
"@vue/test-utils": "^1.0.0",
3637
"tsd": "^0.19.1",
3738
"vue": "^2.6.14",
3839
"vuex": "^3.6.2"

Diff for: packages/typed-vuex/src/accessor.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const getAccessorType = <
1212
>(
1313
store: Partial<NuxtStoreInput<T, G, M, A, S>>
1414
) => {
15-
return {} as MergedStoreType<typeof store & BlankStore>
15+
return (undefined as any) as MergedStoreType<typeof store & BlankStore>
1616
}
1717

1818
const getNestedState = (parent: any, namespaces: string[]): any => {

Diff for: packages/typed-vuex/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ export * from './types/store'
22
export * from './types/actions'
33
export * from './types/getters'
44
export * from './types/mutations'
5+
export * from './types/utils'
56
export { useAccessor, getAccessorType, getAccessorFromStore } from './accessor'
7+
export { createMapper } from './utils'

Diff for: packages/typed-vuex/src/types/utils.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export type ComputedState<T extends Record<string, any>> = {
2+
[K in keyof T]: T[K] extends Function ? T[K] : () => T[K]
3+
}
4+
5+
export interface Mapper<T extends Record<string, any>> {
6+
<M extends keyof T, P extends keyof T[M] = string>(
7+
prop: M,
8+
properties: P[]
9+
): ComputedState<Pick<T[M], P>>
10+
<M extends keyof T, _P extends keyof T[M] = string>(
11+
prop: M | M[]
12+
): ComputedState<Pick<T, M>>
13+
}

Diff for: packages/typed-vuex/src/utils.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
2+
import { Mapper } from './types/utils'
3+
4+
export const createMapper = <T extends Record<string, any>>(accessor: T) => {
5+
const mapper: Mapper<T> = (prop: any, properties?: string[]) => {
6+
if (!properties) {
7+
return Object.fromEntries(
8+
(Array.isArray(prop) ? prop : [prop]).map(property => [
9+
property,
10+
function(...args: any[]) {
11+
const value = accessor
12+
? accessor[property]
13+
: // @ts-ignore
14+
this.$accessor[property]
15+
if (value && typeof value === 'function') return value(...args)
16+
return value
17+
},
18+
])
19+
)
20+
}
21+
return Object.fromEntries(
22+
properties.map(property => [
23+
property,
24+
function(...args: any[]) {
25+
const value =
26+
// @ts-ignore
27+
accessor ? accessor[prop][property] : this.$accessor[prop][property]
28+
if (value && typeof value === 'function') return value(...args)
29+
return value
30+
},
31+
])
32+
)
33+
}
34+
return mapper
35+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`mapper works within a component with injected accessor 1`] = `
4+
"email: \\"\\"
5+
firstName: \\"\\"
6+
fullEmail: \\"\\"
7+
fullName: \\" \\""
8+
`;
9+
10+
exports[`mapper works within a component with injected accessor 2`] = `
11+
"<pre> email: \\"[email protected]\\"
12+
firstName: \\"\\"
13+
fullEmail: \\"[email protected]\\"
14+
fullName: \\" Smith\\"
15+
</pre>"
16+
`;

Diff for: packages/typed-vuex/test/tsd/map-state.test-d.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import Vue from 'vue'
2+
import { getAccessorType, createMapper } from 'typed-vuex'
3+
import { expectType } from 'tsd'
4+
5+
import { getters, state, actions, mutations } from '../fixture'
6+
7+
import * as submodule from '../fixture/submodule'
8+
import { CommitOptions, DispatchOptions } from 'vuex/types/index'
9+
10+
const pattern = {
11+
getters,
12+
state,
13+
actions,
14+
mutations,
15+
modules: {
16+
submodule: {
17+
...submodule,
18+
namespaced: true,
19+
modules: {
20+
nestedSubmodule: {
21+
...submodule,
22+
namespaced: true,
23+
},
24+
},
25+
},
26+
},
27+
}
28+
29+
const accessor = getAccessorType(pattern)
30+
const mapper = createMapper(accessor)
31+
32+
Vue.extend({
33+
computed: {
34+
...mapper(['email']),
35+
...mapper('fullEmail'),
36+
...mapper('submodule', ['fullName']),
37+
},
38+
methods: {
39+
...mapper('submodule', ['setFirstName', 'setName']),
40+
...mapper(['resetEmail', 'initialiseStore']),
41+
test(): void {
42+
// actions
43+
expectType<(options?: DispatchOptions | undefined) => Promise<void>>(
44+
this.resetEmail
45+
)
46+
expectType<
47+
(payload: string, options?: DispatchOptions | undefined) => void
48+
>(this.setName)
49+
// getter
50+
expectType<string>(this.fullEmail)
51+
expectType<string>(this.fullName)
52+
// state
53+
expectType<string>(this.email)
54+
// mutations
55+
expectType<
56+
(payload: string, options?: CommitOptions | undefined) => void
57+
>(this.setFirstName)
58+
expectType<(options?: CommitOptions | undefined) => void>(
59+
this.initialiseStore
60+
)
61+
},
62+
},
63+
})

Diff for: packages/typed-vuex/test/utils.test.ts

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/** @jest-environment jsdom */
2+
3+
import { useAccessor, getAccessorType, createMapper, Mapper } from 'typed-vuex'
4+
import { mount, createLocalVue } from '@vue/test-utils'
5+
import Vuex, { Store } from 'vuex'
6+
7+
import { getters, state, actions, mutations } from './fixture'
8+
import * as submodule from './fixture/submodule'
9+
10+
const pattern = {
11+
getters,
12+
state,
13+
actions,
14+
mutations,
15+
modules: {
16+
submodule: {
17+
...submodule,
18+
namespaced: true,
19+
modules: {
20+
nestedSubmodule: {
21+
...submodule,
22+
namespaced: true,
23+
},
24+
},
25+
},
26+
},
27+
}
28+
29+
const accessorType = getAccessorType(pattern)
30+
31+
describe('mapper', () => {
32+
let store: Store<any>
33+
let accessor: typeof accessorType
34+
let mapper: Mapper<typeof accessorType>
35+
let Vue
36+
// let nuxtMapper: Mapper<typeof accessorType>
37+
38+
beforeEach(() => {
39+
Vue = createLocalVue()
40+
Vue.use(Vuex)
41+
store = new Store(pattern)
42+
accessor = useAccessor(store, pattern)
43+
mapper = createMapper(accessor)
44+
})
45+
46+
test('mapper works with state', () => {
47+
expect(mapper('email').email()).toEqual('')
48+
expect(mapper('submodule', ['firstName']).firstName()).toEqual('')
49+
})
50+
test('mapper works with getters', () => {
51+
expect(mapper('fullEmail').fullEmail()).toEqual('')
52+
expect(mapper('submodule', ['fullName']).fullName()).toEqual(' ')
53+
})
54+
test('mapper works with mutations', () => {
55+
mapper('setEmail').setEmail('[email protected]')
56+
expect(accessor.email).toEqual('[email protected]')
57+
mapper('submodule', ['setLastName']).setLastName('Smith')
58+
expect(accessor.submodule.fullName).toEqual(' Smith')
59+
})
60+
test('mapper works with actions', () => {
61+
mapper('resetEmail').resetEmail()
62+
expect(accessor.email).toEqual('[email protected]')
63+
})
64+
test('works within a component with injected accessor', async () => {
65+
const mapper = createMapper(accessorType)
66+
Vue.prototype.$accessor = accessor
67+
const Component = {
68+
template: `<pre>
69+
email: {{ JSON.stringify(email) }}
70+
firstName: {{ JSON.stringify(firstName) }}
71+
fullEmail: {{ JSON.stringify(fullEmail) }}
72+
fullName: {{ JSON.stringify(fullName) }}
73+
</pre>`,
74+
computed: {
75+
...mapper('email'),
76+
...mapper('submodule', ['firstName']),
77+
...mapper('fullEmail'),
78+
...mapper('submodule', ['fullName']),
79+
},
80+
methods: {
81+
...mapper(['setEmail', 'resetEmail']),
82+
...mapper('submodule', ['setLastName']),
83+
},
84+
}
85+
const wrapper = mount(Component, {
86+
store,
87+
localVue: Vue,
88+
})
89+
expect(wrapper.text()).toMatchSnapshot()
90+
wrapper.vm.setEmail('[email protected]')
91+
wrapper.vm.setLastName('Smith')
92+
await Vue.nextTick()
93+
94+
expect(wrapper.html()).toMatchSnapshot()
95+
})
96+
})

0 commit comments

Comments
 (0)