diff --git a/packages/core/src/sugar/index.ts b/packages/core/src/sugar/index.ts index 7937fb2..9ece7ed 100644 --- a/packages/core/src/sugar/index.ts +++ b/packages/core/src/sugar/index.ts @@ -18,6 +18,7 @@ import { ValidationStage, FailFn, } from './useValidation'; +import { useTransform, SugarUseTransform } from './useTransform'; import { useIsPending, SugarUseIsPending } from './useIsPending'; export class SugarInner { @@ -290,6 +291,11 @@ export class SugarInner { ) => useValidation(this as Sugar, validator, deps)) as SugarUseValidation; + useTransform: SugarUseTransform = ((config: { + forward: (value: T) => Promise; + backward: (value: U) => Promise; + }) => useTransform(this as Sugar, config)) as SugarUseTransform; + useIsPending: SugarUseIsPending = (() => useIsPending(this as Sugar)) as SugarUseIsPending; } diff --git a/packages/core/src/sugar/types.ts b/packages/core/src/sugar/types.ts index 8b9be81..3c53c64 100644 --- a/packages/core/src/sugar/types.ts +++ b/packages/core/src/sugar/types.ts @@ -39,6 +39,7 @@ export type SugarTemplateSetter = ( import type { SugarUseObject } from './useObject'; import type { SugarUseValidation } from './useValidation'; +import type { SugarUseTransform } from './useTransform'; import type { SugarUseIsPending } from './useIsPending'; type SugarType = { @@ -53,6 +54,7 @@ type SugarType = { destroy: () => void; useObject: SugarUseObject; useValidation: SugarUseValidation; + useTransform: SugarUseTransform; useIsPending: SugarUseIsPending; addEventListener: ( type: K, diff --git a/packages/core/src/sugar/useTransform.ts b/packages/core/src/sugar/useTransform.ts new file mode 100644 index 0000000..d434ced --- /dev/null +++ b/packages/core/src/sugar/useTransform.ts @@ -0,0 +1,72 @@ +import { useEffect, useRef } from 'react'; +import { Sugar, SugarGetResult, SugarValue } from './types'; +import { SugarInner } from '.'; + +export type SugarTransformConfig = { + forward: (value: T) => Promise; + backward: (value: U) => Promise; +}; + +export type SugarUseTransform = ( + config: SugarTransformConfig +) => Sugar; + +export function useTransform( + sugar: Sugar, + config: SugarTransformConfig +): Sugar { + const transformedSugar = useRef>(undefined); + + if (!transformedSugar.current) { + transformedSugar.current = new SugarInner({ status: 'pending' }); + } + + useEffect(() => { + transformedSugar.current!.addEventListener('change', () => + sugar.dispatchEvent('change') + ); + transformedSugar.current!.addEventListener('blur', () => + sugar.dispatchEvent('blur') + ); + + sugar.ready( + async (submit) => { + const transformedResult = await transformedSugar.current!.get(submit); + if (transformedResult.result !== 'success') { + return transformedResult as SugarGetResult; + } + const originalValue = await config.backward(transformedResult.value); + return { + result: 'success', + value: originalValue, + }; + }, + async (value) => { + const transformedValue = await config.forward(value); + return await transformedSugar.current!.set(transformedValue); + }, + async (value, executeSet = true) => { + const transformedValue = await config.forward(value); + return await transformedSugar.current!.setTemplate( + transformedValue, + executeSet + ); + } + ); + + const originalTemplate = (sugar as SugarInner).template; + if (originalTemplate?.status === 'pending') { + (transformedSugar.current as SugarInner).setPendingTemplate(); + } else if (originalTemplate?.status === 'resolved') { + config.forward(originalTemplate.value).then((transformedValue) => { + transformedSugar.current!.setTemplate(transformedValue, false); + }); + } + + return () => { + transformedSugar.current!.destroy(); + }; + }, [sugar, config]); + + return transformedSugar.current!; +} diff --git a/tests/core-unittest/src/useTransform-simple.spec.tsx b/tests/core-unittest/src/useTransform-simple.spec.tsx new file mode 100644 index 0000000..79b61a0 --- /dev/null +++ b/tests/core-unittest/src/useTransform-simple.spec.tsx @@ -0,0 +1,52 @@ +import { useForm, TextInput } from '@sugarform/core'; +import { render, renderHook } from '@testing-library/react'; +import { expect, test } from 'vitest'; + +test('useTransform basic functionality', async () => { + const { result: form } = renderHook(() => + useForm({ template: null }) + ); + + const { result: transformedSugar } = renderHook(() => + form.current.sugar.useTransform({ + forward: async (value: string | null) => value ?? '', + backward: async (value: string) => (value === '' ? null : value), + }) + ); + + render(); + + await expect(form.current.sugar.get()).resolves.toStrictEqual({ + result: 'success', + value: null, + }); + + await expect(transformedSugar.current.get()).resolves.toStrictEqual({ + result: 'success', + value: '', + }); + + await transformedSugar.current.set('test'); + + await expect(form.current.sugar.get()).resolves.toStrictEqual({ + result: 'success', + value: 'test', + }); + + await expect(transformedSugar.current.get()).resolves.toStrictEqual({ + result: 'success', + value: 'test', + }); + + await transformedSugar.current.set(''); + + await expect(form.current.sugar.get()).resolves.toStrictEqual({ + result: 'success', + value: null, + }); + + await expect(transformedSugar.current.get()).resolves.toStrictEqual({ + result: 'success', + value: '', + }); +});