1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 02:44:45 +00:00

Merge pull request #62 from robonen/feat/injection-store

feat(packages/vue): useInjectionStore
This commit is contained in:
2025-02-22 23:40:54 +07:00
committed by GitHub
9 changed files with 194 additions and 45 deletions

View File

@@ -13,6 +13,8 @@
* sleep(1000).then(() => { * sleep(1000).then(() => {
* console.log('Hello, World!'); * console.log('Hello, World!');
* }); * });
*
* @since 0.0.3
*/ */
export function sleep(ms: number): Promise<void> { export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));

View File

@@ -2,6 +2,6 @@
"$schema": "https://jsr.io/schema/config-file.v1.json", "$schema": "https://jsr.io/schema/config-file.v1.json",
"name": "@robonen/vue", "name": "@robonen/vue",
"license": "Apache-2.0", "license": "Apache-2.0",
"version": "0.0.4", "version": "0.0.5",
"exports": "./src/index.ts" "exports": "./src/index.ts"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@robonen/vue", "name": "@robonen/vue",
"version": "0.0.4", "version": "0.0.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "Collection of powerful tools for Vue", "description": "Collection of powerful tools for Vue",
"keywords": [ "keywords": [
@@ -16,9 +16,9 @@
"url": "git+https://github.com/robonen/tools.git", "url": "git+https://github.com/robonen/tools.git",
"directory": "./packages/vue" "directory": "./packages/vue"
}, },
"packageManager": "pnpm@9.15.5", "packageManager": "pnpm@10.4.1",
"engines": { "engines": {
"node": ">=22.13.1" "node": ">=22.14.0"
}, },
"type": "module", "type": "module",
"files": [ "files": [

View File

@@ -7,6 +7,7 @@ export * from './useClamp';
export * from './useContextFactory'; export * from './useContextFactory';
export * from './useCounter'; export * from './useCounter';
export * from './useFocusGuard'; export * from './useFocusGuard';
export * from './useInjectionStore';
export * from './useLastChanged'; export * from './useLastChanged';
export * from './useMounted'; export * from './useMounted';
export * from './useOffsetPagination'; export * from './useOffsetPagination';

View File

@@ -72,7 +72,7 @@ describe('useContextFactory', () => {
const childComponent = mount(Child, { const childComponent = mount(Child, {
global: { global: {
plugins: [app => context.provide('test', app)], plugins: [app => context.appProvide(app)('test')],
}, },
}); });

View File

@@ -1,13 +1,13 @@
import {inject, provide, type InjectionKey, type App} from 'vue'; import { inject, provide, type InjectionKey, type App } from 'vue';
import { VueToolsError } from '../..'; import { VueToolsError } from '../..';
/** /**
* @name useContextFactory * @name useContextFactory
* @category Utilities * @category State
* @description A composable that provides a factory for creating context with unique key * @description A composable that provides a factory for creating context with unique key
* *
* @param {string} name The name of the context * @param {string} name The name of the context
* @returns {Object} An object with `inject`, `provide` and `key` properties * @returns {Object} An object with `inject`, `provide`, `appProvide` and `key` properties
* @throws {VueToolsError} when the context is not provided * @throws {VueToolsError} when the context is not provided
* *
* @example * @example
@@ -17,12 +17,12 @@ import { VueToolsError } from '../..';
* const value = inject(); * const value = inject();
* *
* @example * @example
* const { inject: injectContext, provide: provideContext } = useContextFactory('MyContext'); * const { inject: injectContext, appProvide } = useContextFactory('MyContext');
* *
* // In a plugin * // In a plugin
* { * {
* install(app) { * install(app) {
* provideContext('Hello World', app); * appProvide(app)('Hello World');
* } * }
* } * }
* *
@@ -43,14 +43,20 @@ export function useContextFactory<ContextValue>(name: string) {
throw new VueToolsError(`useContextFactory: '${name}' context is not provided`); throw new VueToolsError(`useContextFactory: '${name}' context is not provided`);
}; };
const provideContext = (context: ContextValue, app?: App) => { const provideContext = (context: ContextValue) => {
(app?.provide ?? provide)(injectionKey, context); provide(injectionKey, context);
return context;
};
const appProvide = (app: App) => (context: ContextValue) => {
app.provide(injectionKey, context);
return context; return context;
}; };
return { return {
inject: injectContext, inject: injectContext,
provide: provideContext, provide: provideContext,
appProvide,
key: injectionKey, key: injectionKey,
} }
} }

View File

@@ -0,0 +1,99 @@
import { describe, it, expect } from 'vitest';
import { defineComponent, ref } from 'vue';
import { useInjectionStore } from '.';
import { mount } from '@vue/test-utils';
function testFactory<Args, Return>(
store: ReturnType<typeof useInjectionStore<Args[], Return>>,
) {
const { useProvidingState, useInjectedState } = store;
const Child = defineComponent({
setup() {
const state = useInjectedState();
return { state };
},
template: `{{ state }}`,
});
const Parent = defineComponent({
components: { Child },
setup() {
const state = useProvidingState();
return { state };
},
template: `<Child />`,
});
return {
Parent,
Child,
};
}
describe('useInjectionState', () => {
it('provides and injects state correctly', () => {
const { Parent } = testFactory(
useInjectionStore(() => ref('base'))
);
const wrapper = mount(Parent);
expect(wrapper.text()).toBe('base');
});
it('injects default value when state is not provided', () => {
const { Child } = testFactory(
useInjectionStore(() => ref('without provider'), {
defaultValue: ref('default'),
injectionKey: 'testKey',
})
);
const wrapper = mount(Child);
expect(wrapper.text()).toBe('default');
});
it('provides state at app level', () => {
const injectionStore = useInjectionStore(() => ref('app level'));
const { Child } = testFactory(injectionStore);
const wrapper = mount(Child, {
global: {
plugins: [
app => {
const state = injectionStore.useAppProvidingState(app)();
expect(state.value).toBe('app level');
},
],
},
});
expect(wrapper.text()).toBe('app level');
});
it('works with custom injection key', () => {
const { Parent } = testFactory(
useInjectionStore(() => ref('custom key'), {
injectionKey: Symbol('customKey'),
}),
);
const wrapper = mount(Parent);
expect(wrapper.text()).toBe('custom key');
});
it('handles state factory with arguments', () => {
const injectionStore = useInjectionStore((arg: string) => arg);
const { Child } = testFactory(injectionStore);
const wrapper = mount(Child, {
global: {
plugins: [
app => injectionStore.useAppProvidingState(app)('with args'),
],
},
});
expect(wrapper.text()).toBe('with args');
});
});

View File

@@ -0,0 +1,72 @@
import { inject, provide, type App, type InjectionKey } from 'vue';
export interface useInjectionStoreOptions<Return> {
injectionKey: string | InjectionKey<Return>;
defaultValue?: Return;
}
/**
* @name useInjectionStore
* @category State
* @description Create a global state that can be injected into components
*
* @param {Function} stateFactory A factory function that creates the state
* @param {useInjectionStoreOptions} options An object with the following properties
* @param {string | InjectionKey} options.injectionKey The key to use for the injection
* @param {any} options.defaultValue The default value to use when the state is not provided
* @returns {Object} An object with `useProvidingState`, `useAppProvidingState`, and `useInjectedState` functions
*
* @example
* const { useProvidingState, useInjectedState } = useInjectionStore(() => ref('Hello World'));
*
* // In a parent component
* const state = useProvidingState();
*
* // In a child component
* const state = useInjectedState();
*
* @example
* const { useProvidingState, useInjectedState } = useInjectionStore(() => ref('Hello World'), {
* injectionKey: 'MyState',
* defaultValue: 'Default Value'
* });
*
* // In a plugin
* {
* install(app) {
* const state = useAppProvidingState(app)();
* state.value = 'Hello World';
* }
* }
*
* // In a component
* const state = useInjectedState();
*
* @since 0.0.5
*/
export function useInjectionStore<Args extends any[], Return>(
stateFactory: (...args: Args) => Return,
options?: useInjectionStoreOptions<Return>,
) {
const key = options?.injectionKey ?? Symbol(stateFactory.name ?? 'InjectionStore');
const useProvidingState = (...args: Args) => {
const state = stateFactory(...args);
provide(key, state);
return state;
};
const useAppProvidingState = (app: App) => (...args: Args) => {
const state = stateFactory(...args);
app.provide(key, state);
return state;
};
const useInjectedState = () => inject(key, options?.defaultValue);
return {
useProvidingState,
useAppProvidingState,
useInjectedState
};
}

31
pnpm-lock.yaml generated
View File

@@ -51,15 +51,6 @@ importers:
specifier: ^1.6.3 specifier: ^1.6.3
version: 1.6.3(@algolia/client-search@5.20.0)(@types/node@22.13.1)(postcss@8.5.1)(search-insights@2.13.0)(typescript@5.4.4) version: 1.6.3(@algolia/client-search@5.20.0)(@types/node@22.13.1)(postcss@8.5.1)(search-insights@2.13.0)(typescript@5.4.4)
apps/vhs:
devDependencies:
'@robonen/tsconfig':
specifier: workspace:*
version: link:../../packages/tsconfig
'@types/bun':
specifier: ^1.2.2
version: 1.2.2
packages/platform: packages/platform:
devDependencies: devDependencies:
'@robonen/tsconfig': '@robonen/tsconfig':
@@ -1817,9 +1808,6 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
'@types/bun@1.2.2':
resolution: {integrity: sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w==}
'@types/bunyan@1.8.9': '@types/bunyan@1.8.9':
resolution: {integrity: sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==} resolution: {integrity: sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==}
@@ -1898,9 +1886,6 @@ packages:
'@types/web-bluetooth@0.0.20': '@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
'@types/ws@8.5.12':
resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==}
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@@ -2234,9 +2219,6 @@ packages:
builtins@5.1.0: builtins@5.1.0:
resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==}
bun-types@1.2.2:
resolution: {integrity: sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg==}
bunyan@1.8.15: bunyan@1.8.15:
resolution: {integrity: sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==} resolution: {integrity: sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==}
engines: {'0': node >=0.10.0} engines: {'0': node >=0.10.0}
@@ -7314,10 +7296,6 @@ snapshots:
'@trysound/sax@0.2.0': {} '@trysound/sax@0.2.0': {}
'@types/bun@1.2.2':
dependencies:
bun-types: 1.2.2
'@types/bunyan@1.8.9': '@types/bunyan@1.8.9':
dependencies: dependencies:
'@types/node': 22.13.1 '@types/node': 22.13.1
@@ -7392,10 +7370,6 @@ snapshots:
'@types/web-bluetooth@0.0.20': {} '@types/web-bluetooth@0.0.20': {}
'@types/ws@8.5.12':
dependencies:
'@types/node': 22.13.1
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 22.13.1 '@types/node': 22.13.1
@@ -7797,11 +7771,6 @@ snapshots:
dependencies: dependencies:
semver: 7.7.0 semver: 7.7.0
bun-types@1.2.2:
dependencies:
'@types/node': 22.13.1
'@types/ws': 8.5.12
bunyan@1.8.15: bunyan@1.8.15:
optionalDependencies: optionalDependencies:
dtrace-provider: 0.8.8 dtrace-provider: 0.8.8