From: Eduardo San Martin Morote Date: Mon, 25 Nov 2019 13:17:07 +0000 (+0100) Subject: feat: add getters X-Git-Tag: v0.0.1~9 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ad5075589e9e52a3559f0800421faf0430fed674;p=thirdparty%2Fvuejs%2Fpinia.git feat: add getters --- diff --git a/__tests__/getters.spec.ts b/__tests__/getters.spec.ts new file mode 100644 index 00000000..cdab1b7d --- /dev/null +++ b/__tests__/getters.spec.ts @@ -0,0 +1,28 @@ +import { createStore } from '../src' + +describe('Store', () => { + function buildStore() { + return createStore( + 'main', + () => ({ + name: 'Eduardo', + }), + { + upperCaseName: ({ name }) => name.toUpperCase(), + } + ) + } + + it('adds getters to the store', () => { + const store = buildStore() + expect(store.upperCaseName.value).toBe('EDUARDO') + store.state.name = 'Ed' + expect(store.upperCaseName.value).toBe('ED') + }) + + it('updates the value', () => { + const store = buildStore() + store.state.name = 'Ed' + expect(store.upperCaseName.value).toBe('ED') + }) +}) diff --git a/rollup.config.js b/rollup.config.js index db0b321a..4f8b7811 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -49,6 +49,10 @@ function createEntry({ banner, file: `dist/${pkg.name}.UNKNOWN.js`, format, + + globals: { + '@vue/composition-api': 'VueCompositionApi', + }, }, } @@ -65,9 +69,6 @@ function createEntry({ config.plugins.push(resolve(), commonjs()) } else { config.external = external - config.globals = { - '@vue/composition-api': 'VueCompositionApi', - } } config.plugins.push( diff --git a/src/index.ts b/src/index.ts index 11e9101f..0377b9e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { ref, watch } from '@vue/composition-api' +import { ref, watch, computed } from '@vue/composition-api' import { Ref } from '@vue/composition-api/dist/reactivity' import { StateTree, @@ -6,6 +6,8 @@ import { SubscriptionCallback, DeepPartial, isPlainObject, + StoreGetters, + StoreGetter, } from './types' import { devtoolPlugin } from './devtools' @@ -33,17 +35,28 @@ function innerPatch( * they want, no? like user/cart */ +type CombinedStore< + Id extends string, + S extends StateTree, + G extends Record> +> = Store & StoreGetters + /** * Creates a store instance * @param id unique identifier of the store, like a name. eg: main, cart, user * @param initialState initial state applied to the store, Must be correctly typed to infer typings */ -export function createStore( +export function createStore< + Id extends string, + S extends StateTree, + G extends Record> +>( id: Id, - buildState: () => S + buildState: () => S, + getters: G // methods: Record -): Store { +): CombinedStore { const state: Ref = ref(buildState()) function replaceState(newState: S) { state.value = newState @@ -84,7 +97,7 @@ export function createStore( // TODO: return function to remove subscription } - const store: Store = { + const storeWithState: Store = { id, // it is replaced below by a getter state: state.value, @@ -98,6 +111,21 @@ export function createStore( }, } + // @ts-ignore we have to build it + const computedGetters: StoreGetters = {} + for (const getterName in getters) { + const method = getters[getterName] + // @ts-ignore + computedGetters[getterName] = computed>(() => + getters[getterName](state.value) + ) + } + + const store = { + ...storeWithState, + ...computedGetters, + } + // make state access invisible Object.defineProperty(store, 'state', { get: () => state.value, @@ -121,14 +149,15 @@ export function createStore( * @param buildState function that returns a state */ -export function makeStore( - id: Id, - buildState: () => S -) { - let store: Store | undefined +export function makeStore< + Id extends string, + S extends StateTree, + G extends Record> +>(id: Id, buildState: () => S, getters: G) { + let store: CombinedStore | undefined - function useStore(): Store { - if (!store) store = createStore(id, buildState) + function useStore(): CombinedStore { + if (!store) store = createStore(id, buildState, getters) return store } @@ -144,9 +173,17 @@ export function makeStore( } // export const store = createStore('main', initialState) -// export const cartStore = createStore('cart', { + +// type StateI = ReturnType +// const buildState = () => ({ // items: ['thing 1'], // }) +// export const cartStore = createStore('cart', buildState, { +// amount: state => state.items.length, +// }) + +// cartStore.nonueo +// cartStore.amount.value * 2 // store.patch({ // toggle: 'off', diff --git a/src/types.ts b/src/types.ts index 76cce8eb..431de432 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import { Ref } from '@vue/composition-api' + interface JSONSerializable { toJSON(): string } @@ -40,7 +42,11 @@ export function isPlainObject( // eslint-disable-next-line @typescript-eslint/no-empty-interface interface StateTreeArray extends Array {} -// type TODO = any +export interface StoreGetter { + (state: S): T +} + +type TODO = any // type StoreMethod = TODO export type DeepPartial = { [K in keyof T]?: DeepPartial } // type DeepReadonly = { readonly [P in keyof T]: DeepReadonly } @@ -50,6 +56,13 @@ export type SubscriptionCallback = ( state: S ) => void +export type StoreGetters< + S extends StateTree, + G extends Record> +> = { + [k in keyof G]: G[k] extends StoreGetter ? Ref : never +} + export interface Store { /** * Unique identifier of the store @@ -60,6 +73,7 @@ export interface Store { * State of the Store */ state: S + /** * Applies a state patch to current state. Allows passing nested values * @param partialState patch to apply to the state