diff --git a/packages/stdlib/src/async/sleep/index.ts b/packages/stdlib/src/async/sleep/index.ts index 94a50d9..db3f5fc 100644 --- a/packages/stdlib/src/async/sleep/index.ts +++ b/packages/stdlib/src/async/sleep/index.ts @@ -13,6 +13,8 @@ * sleep(1000).then(() => { * console.log('Hello, World!'); * }); + * + * @since 0.0.3 */ export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/vue/jsr.json b/packages/vue/jsr.json index fa8fa10..e23ba1d 100644 --- a/packages/vue/jsr.json +++ b/packages/vue/jsr.json @@ -2,6 +2,6 @@ "$schema": "https://jsr.io/schema/config-file.v1.json", "name": "@robonen/vue", "license": "Apache-2.0", - "version": "0.0.4", + "version": "0.0.5", "exports": "./src/index.ts" } \ No newline at end of file diff --git a/packages/vue/package.json b/packages/vue/package.json index 43ee381..056aa5c 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@robonen/vue", - "version": "0.0.4", + "version": "0.0.5", "license": "Apache-2.0", "description": "Collection of powerful tools for Vue", "keywords": [ @@ -16,9 +16,9 @@ "url": "git+https://github.com/robonen/tools.git", "directory": "./packages/vue" }, - "packageManager": "pnpm@9.15.5", + "packageManager": "pnpm@10.4.1", "engines": { - "node": ">=22.13.1" + "node": ">=22.14.0" }, "type": "module", "files": [ diff --git a/packages/vue/src/composables/index.ts b/packages/vue/src/composables/index.ts index ffc3866..6fa926b 100644 --- a/packages/vue/src/composables/index.ts +++ b/packages/vue/src/composables/index.ts @@ -7,6 +7,7 @@ export * from './useClamp'; export * from './useContextFactory'; export * from './useCounter'; export * from './useFocusGuard'; +export * from './useInjectionStore'; export * from './useLastChanged'; export * from './useMounted'; export * from './useOffsetPagination'; diff --git a/packages/vue/src/composables/useContextFactory/index.test.ts b/packages/vue/src/composables/useContextFactory/index.test.ts index ad26192..5272ffe 100644 --- a/packages/vue/src/composables/useContextFactory/index.test.ts +++ b/packages/vue/src/composables/useContextFactory/index.test.ts @@ -72,7 +72,7 @@ describe('useContextFactory', () => { const childComponent = mount(Child, { global: { - plugins: [app => context.provide('test', app)], + plugins: [app => context.appProvide(app)('test')], }, }); diff --git a/packages/vue/src/composables/useContextFactory/index.ts b/packages/vue/src/composables/useContextFactory/index.ts index c258d52..4428423 100644 --- a/packages/vue/src/composables/useContextFactory/index.ts +++ b/packages/vue/src/composables/useContextFactory/index.ts @@ -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 '../..'; /** * @name useContextFactory - * @category Utilities + * @category State * @description A composable that provides a factory for creating context with unique key * * @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 * * @example @@ -17,12 +17,12 @@ import { VueToolsError } from '../..'; * const value = inject(); * * @example - * const { inject: injectContext, provide: provideContext } = useContextFactory('MyContext'); + * const { inject: injectContext, appProvide } = useContextFactory('MyContext'); * * // In a plugin * { * install(app) { - * provideContext('Hello World', app); + * appProvide(app)('Hello World'); * } * } * @@ -31,26 +31,32 @@ import { VueToolsError } from '../..'; * * @since 0.0.1 */ -export function useContextFactory(name: string) { +export function useContextFactory(name: string) { const injectionKey: InjectionKey = Symbol(name); const injectContext = (fallback?: Fallback) => { const context = inject(injectionKey, fallback); if (context !== undefined) - return context; + return context; throw new VueToolsError(`useContextFactory: '${name}' context is not provided`); }; - const provideContext = (context: ContextValue, app?: App) => { - (app?.provide ?? provide)(injectionKey, context); + const provideContext = (context: ContextValue) => { + provide(injectionKey, context); + return context; + }; + + const appProvide = (app: App) => (context: ContextValue) => { + app.provide(injectionKey, context); return context; }; return { inject: injectContext, provide: provideContext, + appProvide, key: injectionKey, } } \ No newline at end of file diff --git a/packages/vue/src/composables/useInjectionStore/index.test.ts b/packages/vue/src/composables/useInjectionStore/index.test.ts new file mode 100644 index 0000000..0c42e08 --- /dev/null +++ b/packages/vue/src/composables/useInjectionStore/index.test.ts @@ -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( + store: ReturnType>, +) { + 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: ``, + }); + + 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'); + }); +}); diff --git a/packages/vue/src/composables/useInjectionStore/index.ts b/packages/vue/src/composables/useInjectionStore/index.ts new file mode 100644 index 0000000..5134f82 --- /dev/null +++ b/packages/vue/src/composables/useInjectionStore/index.ts @@ -0,0 +1,72 @@ +import { inject, provide, type App, type InjectionKey } from 'vue'; + +export interface useInjectionStoreOptions { + injectionKey: string | InjectionKey; + 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( + stateFactory: (...args: Args) => Return, + options?: useInjectionStoreOptions, +) { + 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 + }; +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2c0926..20df552 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,15 +51,6 @@ importers: 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) - apps/vhs: - devDependencies: - '@robonen/tsconfig': - specifier: workspace:* - version: link:../../packages/tsconfig - '@types/bun': - specifier: ^1.2.2 - version: 1.2.2 - packages/platform: devDependencies: '@robonen/tsconfig': @@ -1817,9 +1808,6 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@types/bun@1.2.2': - resolution: {integrity: sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w==} - '@types/bunyan@1.8.9': resolution: {integrity: sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==} @@ -1898,9 +1886,6 @@ packages: '@types/web-bluetooth@0.0.20': resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} - '@types/ws@8.5.12': - resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} - '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -2234,9 +2219,6 @@ packages: builtins@5.1.0: resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} - bun-types@1.2.2: - resolution: {integrity: sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg==} - bunyan@1.8.15: resolution: {integrity: sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==} engines: {'0': node >=0.10.0} @@ -7314,10 +7296,6 @@ snapshots: '@trysound/sax@0.2.0': {} - '@types/bun@1.2.2': - dependencies: - bun-types: 1.2.2 - '@types/bunyan@1.8.9': dependencies: '@types/node': 22.13.1 @@ -7392,10 +7370,6 @@ snapshots: '@types/web-bluetooth@0.0.20': {} - '@types/ws@8.5.12': - dependencies: - '@types/node': 22.13.1 - '@types/yauzl@2.10.3': dependencies: '@types/node': 22.13.1 @@ -7797,11 +7771,6 @@ snapshots: dependencies: semver: 7.7.0 - bun-types@1.2.2: - dependencies: - '@types/node': 22.13.1 - '@types/ws': 8.5.12 - bunyan@1.8.15: optionalDependencies: dtrace-provider: 0.8.8