From dfda5a4f5800fd6a0b4606465e7b403f6fcb9d3a Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 3 Apr 2025 08:34:29 +0300 Subject: [PATCH 01/18] refactor(vue-form): sync with solid js --- .../src/{useField.tsx => useField.ts} | 562 ++++++++++-------- .../vue-form/src/{useForm.tsx => useForm.ts} | 229 +++---- packages/vue-form/src/utils.ts | 1 + packages/vue-form/tests/useField.test.tsx | 20 +- packages/vue-form/tests/useForm.test.tsx | 42 +- 5 files changed, 468 insertions(+), 386 deletions(-) rename packages/vue-form/src/{useField.tsx => useField.ts} (78%) rename packages/vue-form/src/{useForm.tsx => useForm.ts} (66%) create mode 100644 packages/vue-form/src/utils.ts diff --git a/packages/vue-form/src/useField.tsx b/packages/vue-form/src/useField.ts similarity index 78% rename from packages/vue-form/src/useField.tsx rename to packages/vue-form/src/useField.ts index c63631431..682814906 100644 --- a/packages/vue-form/src/useField.tsx +++ b/packages/vue-form/src/useField.ts @@ -1,6 +1,13 @@ import { FieldApi } from '@tanstack/form-core' -import { useStore } from '@tanstack/vue-store' -import { defineComponent, onMounted, onUnmounted, watch } from 'vue' +import { + defineComponent, + onBeforeUnmount, + onMounted, + shallowRef, + triggerRef, + watchEffect, +} from 'vue' +import { NOOP } from './utils' import type { DeepKeys, DeepValue, @@ -10,17 +17,95 @@ import type { FormValidateOrFn, } from '@tanstack/form-core' import type { - ComponentOptionsMixin, - CreateComponentPublicInstanceWithMixins, + DefineSetupFnComponent, EmitsOptions, EmitsToProps, PublicProps, Ref, SetupContext, SlotsType, + VNode, } from 'vue' import type { UseFieldOptions, UseFieldOptionsBound } from './types' +export type FieldComponentProps< + TParentData, + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TFormOnMount extends undefined | FormValidateOrFn, + TFormOnChange extends undefined | FormValidateOrFn, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TFormOnBlur extends undefined | FormValidateOrFn, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TFormOnSubmit extends undefined | FormValidateOrFn, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TFormOnServer extends undefined | FormAsyncValidateOrFn, + TParentSubmitMeta, +> = UseFieldOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta +> + +export type FieldComponentBoundProps< + TParentData, + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, +> = UseFieldOptionsBound< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync +> + export type FieldComponent< TParentData, TFormOnMount extends undefined | FormValidateOrFn, @@ -65,76 +150,68 @@ export type FieldComponent< > & EmitsToProps & PublicProps, -) => CreateComponentPublicInstanceWithMixins< - FieldComponentBoundProps< - TParentData, - TName, - TData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync - >, - {}, - {}, - {}, - {}, - ComponentOptionsMixin, - ComponentOptionsMixin, - EmitsOptions, - PublicProps, - {}, - false, - {}, - SlotsType<{ - default: { - field: FieldApi< - TParentData, - TName, - TData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TFormOnMount, - TFormOnChange, - TFormOnChangeAsync, - TFormOnBlur, - TFormOnBlurAsync, - TFormOnSubmit, - TFormOnSubmitAsync, - TFormOnServer, - TParentSubmitMeta - > - state: FieldApi< - TParentData, - TName, - TData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TFormOnMount, - TFormOnChange, - TFormOnChangeAsync, - TFormOnBlur, - TFormOnBlurAsync, - TFormOnSubmit, - TFormOnSubmitAsync, - TFormOnServer, - TParentSubmitMeta - >['state'] - } - }> +) => InstanceType< + DefineSetupFnComponent< + FieldComponentBoundProps< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync + >, + [], + SlotsType<{ + default?: (slotProps: { + field: FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + > + state: FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + >['state'] + }) => VNode[] + }> + > > export interface VueFieldApi< @@ -191,7 +268,7 @@ export type UseField< | undefined | FieldAsyncValidateOrFn, >( - opts: UseFieldOptionsBound< + opts: () => UseFieldOptionsBound< TParentData, TName, TData, @@ -264,6 +341,73 @@ export type UseField< > } +export const Field = defineComponent( + < + TParentData, + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TFormOnMount extends undefined | FormValidateOrFn, + TFormOnChange extends undefined | FormValidateOrFn, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TFormOnBlur extends undefined | FormValidateOrFn, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TFormOnSubmit extends undefined | FormValidateOrFn, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TFormOnServer extends undefined | FormAsyncValidateOrFn, + TParentSubmitMeta, + >( + _: UseFieldOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + >, + context: SetupContext, + ) => { + const { api, state } = useField(() => ({ + ...(context.attrs as any), + })) + + return () => + context.slots.default!({ + field: api, + state: state.value, + }) + }, + { + name: 'Field', + inheritAttrs: false, + }, +) + export function useField< TParentData, TName extends DeepKeys, @@ -291,7 +435,7 @@ export function useField< TFormOnServer extends undefined | FormAsyncValidateOrFn, TParentSubmitMeta, >( - opts: UseFieldOptions< + opts: () => UseFieldOptions< TParentData, TName, TData, @@ -312,17 +456,53 @@ export function useField< TFormOnServer, TParentSubmitMeta >, -) { - const fieldApi = (() => { - const api = new FieldApi({ - ...opts, - form: opts.form, - name: opts.name, - }) - - const extendedApi: typeof api & - VueFieldApi< +): { + api: FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + > & + VueFieldApi< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + > + state: Readonly< + Ref< + FieldApi< TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, TFormOnMount, TFormOnChange, TFormOnChangeAsync, @@ -332,152 +512,33 @@ export function useField< TFormOnSubmitAsync, TFormOnServer, TParentSubmitMeta - > = api as never - - extendedApi.Field = Field as never - - return extendedApi - })() - - const fieldState = useStore(fieldApi.store, (state) => state) - - let cleanup!: () => void - onMounted(() => { - cleanup = fieldApi.mount() - }) - - onUnmounted(() => { - cleanup() - }) - - watch( - () => opts, - () => { - // Keep options up to date as they are rendered - fieldApi.update({ ...opts, form: opts.form } as never) - }, - ) - - return { api: fieldApi, state: fieldState } as const -} - -export type FieldComponentProps< - TParentData, - TName extends DeepKeys, - TData extends DeepValue, - TOnMount extends undefined | FieldValidateOrFn, - TOnChange extends undefined | FieldValidateOrFn, - TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, - TOnBlur extends undefined | FieldValidateOrFn, - TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, - TOnSubmit extends undefined | FieldValidateOrFn, - TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, - TFormOnMount extends undefined | FormValidateOrFn, - TFormOnChange extends undefined | FormValidateOrFn, - TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, - TFormOnBlur extends undefined | FormValidateOrFn, - TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, - TFormOnSubmit extends undefined | FormValidateOrFn, - TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, - TFormOnServer extends undefined | FormAsyncValidateOrFn, - TParentSubmitMeta, -> = UseFieldOptions< - TParentData, - TName, - TData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TFormOnMount, - TFormOnChange, - TFormOnChangeAsync, - TFormOnBlur, - TFormOnBlurAsync, - TFormOnSubmit, - TFormOnSubmitAsync, - TFormOnServer, - TParentSubmitMeta -> - -export type FieldComponentBoundProps< - TParentData, - TName extends DeepKeys, - TData extends DeepValue, - TOnMount extends undefined | FieldValidateOrFn, - TOnChange extends undefined | FieldValidateOrFn, - TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, - TOnBlur extends undefined | FieldValidateOrFn, - TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, - TOnSubmit extends undefined | FieldValidateOrFn, - TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, -> = UseFieldOptionsBound< - TParentData, - TName, - TData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync -> - -export const Field = defineComponent( - < + >['state'] + > + > +} { + let api: FieldApi< TParentData, - TName extends DeepKeys, - TData extends DeepValue, - TOnMount extends undefined | FieldValidateOrFn, - TOnChange extends undefined | FieldValidateOrFn, - TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, - TOnBlur extends undefined | FieldValidateOrFn, - TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, - TOnSubmit extends undefined | FieldValidateOrFn, - TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, - TFormOnMount extends undefined | FormValidateOrFn, - TFormOnChange extends undefined | FormValidateOrFn, - TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, - TFormOnBlur extends undefined | FormValidateOrFn, - TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, - TFormOnSubmit extends undefined | FormValidateOrFn, - TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, - TFormOnServer extends undefined | FormAsyncValidateOrFn, - TParentSubmitMeta, - >( - fieldOptions: UseFieldOptions< + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + > & + VueFieldApi< TParentData, - TName, - TData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, TFormOnMount, TFormOnChange, TFormOnChangeAsync, @@ -487,16 +548,33 @@ export const Field = defineComponent( TFormOnSubmitAsync, TFormOnServer, TParentSubmitMeta - >, - context: SetupContext, - ) => { - const fieldApi = useField({ ...fieldOptions, ...context.attrs }) + > - return () => - context.slots.default!({ - field: fieldApi.api, - state: fieldApi.state.value, - }) - }, - { name: 'Field', inheritAttrs: false }, -) + watchEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!api) { + api = new FieldApi(opts()) as never + api.Field = Field as never + } else { + api.update(opts()) + } + }) + + let cleanup = NOOP + const state = shallowRef(api!.store.state) + const unsubscribeStore = api!.store.subscribe(() => triggerRef(state)) + + onMounted(() => { + cleanup = api.mount() + }) + + onBeforeUnmount(() => { + cleanup() + unsubscribeStore() + }) + + return { + api: api! as never, + state, + } +} diff --git a/packages/vue-form/src/useForm.tsx b/packages/vue-form/src/useForm.ts similarity index 66% rename from packages/vue-form/src/useForm.tsx rename to packages/vue-form/src/useForm.ts index 616fb0d58..230e15d65 100644 --- a/packages/vue-form/src/useForm.tsx +++ b/packages/vue-form/src/useForm.ts @@ -1,24 +1,25 @@ -import { FormApi } from '@tanstack/form-core' import { useStore } from '@tanstack/vue-store' -import { defineComponent, h, onMounted } from 'vue' +import { FormApi } from '@tanstack/form-core' +import { defineComponent, h, onMounted, onScopeDispose, watchEffect } from 'vue' import { Field, useField } from './useField' +import { NOOP } from './utils' +import type { FieldComponent, UseField } from './useField' import type { - FormAsyncValidateOrFn, - FormOptions, - FormState, - FormValidateOrFn, -} from '@tanstack/form-core' -import type { NoInfer } from '@tanstack/vue-store' -import type { - ComponentOptionsMixin, - CreateComponentPublicInstanceWithMixins, + DefineSetupFnComponent, EmitsOptions, EmitsToProps, PublicProps, Ref, SlotsType, + VNode, } from 'vue' -import type { FieldComponent, UseField } from './useField' +import type { + FormAsyncValidateOrFn, + FormOptions, + FormState, + FormValidateOrFn, +} from '@tanstack/form-core' +import type { NoInfer } from '@tanstack/vue-store' type SubscribeComponent< TParentData, @@ -66,50 +67,44 @@ type SubscribeComponent< ) => TSelected } & EmitsToProps & PublicProps, - ) => CreateComponentPublicInstanceWithMixins< - { - selector?: ( - state: NoInfer< - FormState< - TParentData, - TFormOnMount, - TFormOnChange, - TFormOnChangeAsync, - TFormOnBlur, - TFormOnBlurAsync, - TFormOnSubmit, - TFormOnSubmitAsync, - TFormOnServer - > - >, - ) => TSelected - }, - {}, - {}, - {}, - {}, - ComponentOptionsMixin, - ComponentOptionsMixin, - EmitsOptions, - PublicProps, - {}, - false, - {}, - SlotsType<{ - default: NoInfer< - FormState< - TParentData, - TFormOnMount, - TFormOnChange, - TFormOnChangeAsync, - TFormOnBlur, - TFormOnBlurAsync, - TFormOnSubmit, - TFormOnSubmitAsync, - TFormOnServer - > - > - }> + ) => InstanceType< + DefineSetupFnComponent< + { + selector?: ( + state: NoInfer< + FormState< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer + > + >, + ) => TSelected + }, + [], + SlotsType<{ + default?: ( + slotProps: NoInfer< + FormState< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer + > + >, + ) => VNode[] + }> + > > export interface VueFormApi< @@ -204,7 +199,7 @@ export function useForm< TFormOnServer extends undefined | FormAsyncValidateOrFn, TSubmitMeta, >( - opts?: FormOptions< + opts?: () => FormOptions< TParentData, TFormOnMount, TFormOnChange, @@ -217,8 +212,27 @@ export function useForm< TSubmitMeta >, ) { - const formApi = (() => { - const api = new FormApi< + let api: FormApi< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TSubmitMeta + > + + watchEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!api) api = new FormApi(opts?.()) + api.update(opts?.()) + }) + + const extendedApi: typeof api & + VueFormApi< TParentData, TFormOnMount, TFormOnChange, @@ -229,62 +243,51 @@ export function useForm< TFormOnSubmitAsync, TFormOnServer, TSubmitMeta - >(opts) - - const extendedApi: typeof api & - VueFormApi< - TParentData, - TFormOnMount, - TFormOnChange, - TFormOnChangeAsync, - TFormOnBlur, - TFormOnBlurAsync, - TFormOnSubmit, - TFormOnSubmitAsync, - TFormOnServer, - TSubmitMeta - > = api as never - extendedApi.Field = defineComponent( - (props, context) => { - return () => - h( - Field as never, - { ...props, ...context.attrs, form: api }, - context.slots, - ) - }, - { - name: 'APIField', - inheritAttrs: false, - }, - ) as never - extendedApi.useField = (props) => { - const field = useField({ ...props, form: api }) - return field - } - extendedApi.useStore = (selector) => { - return useStore(api.store as never, selector as never) as never - } - extendedApi.Subscribe = defineComponent( - (props, context) => { - const allProps = { ...props, ...context.attrs } - const selector = allProps.selector ?? ((state: never) => state) - const data = useStore(api.store as never, selector as never) - return () => context.slots.default!(data.value) - }, - { - name: 'Subscribe', - inheritAttrs: false, - }, - ) as never + > = api! as never - return extendedApi - })() + extendedApi.Field = defineComponent( + (props, context) => { + return () => + h( + Field as never, + { ...props, ...context.attrs, form: api }, + context.slots, + ) + }, + { + name: 'APIField', + inheritAttrs: false, + }, + ) as never - onMounted(formApi.mount) + extendedApi.useField = (props) => { + return useField(() => ({ ...props(), form: api })) as never + } + extendedApi.useStore = (selector) => { + return useStore(api.store as never, selector as never) as never + } + extendedApi.Subscribe = defineComponent( + (props, context) => { + const data = useStore(api.store as never, props.selector as never) + return () => context.slots.default!(data.value) + }, + { + name: 'Subscribe', + inheritAttrs: false, + props: { + selector: { + type: Function, + default: undefined, + }, + }, + }, + ) as never - // formApi.useStore((state) => state.isSubmitting) - formApi.update(opts) + let cleanup = NOOP + onMounted(() => { + cleanup = extendedApi.mount() + }) + onScopeDispose(cleanup) - return formApi + return extendedApi } diff --git a/packages/vue-form/src/utils.ts b/packages/vue-form/src/utils.ts new file mode 100644 index 000000000..8e6667278 --- /dev/null +++ b/packages/vue-form/src/utils.ts @@ -0,0 +1 @@ +export const NOOP = () => {} diff --git a/packages/vue-form/tests/useField.test.tsx b/packages/vue-form/tests/useField.test.tsx index 97f2e85f7..86723f069 100644 --- a/packages/vue-form/tests/useField.test.tsx +++ b/packages/vue-form/tests/useField.test.tsx @@ -16,9 +16,9 @@ describe('useField', () => { } const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: {} as Person, - }) + })) return () => ( @@ -49,7 +49,7 @@ describe('useField', () => { const error = 'Please enter a different value' const Comp = defineComponent(() => { - const form = useForm({ defaultValues: {} as Person }) + const form = useForm(() => ({ defaultValues: {} as Person })) return () => ( { const error = 'Please enter a different value' const Comp = defineComponent(() => { - const form = useForm({ defaultValues: {} as Person }) + const form = useForm(() => ({ defaultValues: {} as Person })) return () => ( { const error = 'Please enter a different value' const Comp = defineComponent(() => { - const form = useForm({ defaultValues: {} as Person }) + const form = useForm(() => ({ defaultValues: {} as Person })) return () => ( { const error = 'Please enter a different value' const Comp = defineComponent(() => { - const form = useForm({ defaultValues: {} as Person }) + const form = useForm(() => ({ defaultValues: {} as Person })) return () => ( { type CompVal = { people: Array } const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: { people: [], } as CompVal, onSubmit: ({ value }) => fn(value), - }) + })) return () => (
@@ -318,12 +318,12 @@ describe('useField', () => { type CompVal = { people: Array<{ age: number; name: string }> } const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: { people: [], } as CompVal, onSubmit: ({ value }) => fn(value), - }) + })) return () => (
diff --git a/packages/vue-form/tests/useForm.test.tsx b/packages/vue-form/tests/useForm.test.tsx index 7d03f5896..06184cd7e 100644 --- a/packages/vue-form/tests/useForm.test.tsx +++ b/packages/vue-form/tests/useForm.test.tsx @@ -17,7 +17,7 @@ type Person = { describe('useForm', () => { it('preserved field state', async () => { const Comp = defineComponent(() => { - const form = useForm({ defaultValues: {} as Person }) + const form = useForm(() => ({ defaultValues: {} as Person })) return () => ( @@ -44,12 +44,12 @@ describe('useForm', () => { it('should allow default values to be set', async () => { const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: { firstName: 'FirstName', lastName: 'LastName', } as Person, - }) + })) return () => ( @@ -67,14 +67,14 @@ describe('useForm', () => { const Comp = defineComponent(() => { const submittedData = ref<{ firstName: string }>() - const form = useForm({ + const form = useForm(() => ({ defaultValues: { firstName: 'FirstName', }, onSubmit: ({ value }) => { submittedData.value = value }, - }) + })) return () => (
@@ -115,7 +115,7 @@ describe('useForm', () => { const formMounted = ref(false) const mountForm = ref(false) - const form = useForm({ + const form = useForm(() => ({ defaultValues: { firstName: 'FirstName', }, @@ -125,7 +125,7 @@ describe('useForm', () => { return undefined }, }, - }) + })) return () => mountForm.value ? ( @@ -146,14 +146,14 @@ describe('useForm', () => { const error = 'Please enter a different value' const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: {} as Person, validators: { onChange() { return error }, }, - }) + })) return () => (
@@ -188,13 +188,13 @@ describe('useForm', () => { const error = 'Please enter a different value' const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: {} as Person, validators: { onChange: ({ value }) => value.firstName === 'other' ? error : undefined, }, - }) + })) const errors = form.useStore((s) => s.errors) @@ -232,13 +232,13 @@ describe('useForm', () => { const error = 'Please enter a different value' const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: {} as Person, validators: { onChange: ({ value }) => value.firstName === 'other' ? error : undefined, }, - }) + })) const errors = form.useStore((s) => s.errorMap) @@ -276,7 +276,7 @@ describe('useForm', () => { const onBlurError = 'Please enter a different value (onBlurError)' const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: { firstName: '', }, @@ -290,7 +290,7 @@ describe('useForm', () => { return undefined }, }, - }) + })) const errors = form.useStore((s) => s.errorMap) @@ -330,7 +330,7 @@ describe('useForm', () => { const error = 'Please enter a different value' const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: {} as Person, validators: { onChangeAsync: async () => { @@ -338,7 +338,7 @@ describe('useForm', () => { return error }, }, - }) + })) const errors = form.useStore((s) => s.errorMap) @@ -377,7 +377,7 @@ describe('useForm', () => { const onBlurError = 'Please enter a different value (onBlurError)' const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: {} as Person, validators: { onChangeAsync: async () => { @@ -389,7 +389,7 @@ describe('useForm', () => { return onBlurError }, }, - }) + })) const errors = form.useStore((s) => s.errorMap) return () => ( @@ -433,7 +433,7 @@ describe('useForm', () => { const error = 'Please enter a different value' const Comp = defineComponent(() => { - const form = useForm({ + const form = useForm(() => ({ defaultValues: {} as Person, validators: { onChangeAsyncDebounceMs: 100, @@ -443,7 +443,7 @@ describe('useForm', () => { return error }, }, - }) + })) const errors = form.useStore((s) => s.errors) return () => ( From 1f944ba2c791afb980d2b60565fbffbc79669b0b Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 3 Apr 2025 09:01:23 +0300 Subject: [PATCH 02/18] chore: store subscribe --- packages/vue-form/src/useField.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vue-form/src/useField.ts b/packages/vue-form/src/useField.ts index 682814906..23f603249 100644 --- a/packages/vue-form/src/useField.ts +++ b/packages/vue-form/src/useField.ts @@ -7,6 +7,7 @@ import { triggerRef, watchEffect, } from 'vue' +import { shallow } from '@tanstack/vue-store' import { NOOP } from './utils' import type { DeepKeys, @@ -562,7 +563,10 @@ export function useField< let cleanup = NOOP const state = shallowRef(api!.store.state) - const unsubscribeStore = api!.store.subscribe(() => triggerRef(state)) + const unsubscribeStore = api!.store.subscribe(() => { + if (shallow(state.value, api!.store.state)) return + triggerRef(state) + }) onMounted(() => { cleanup = api.mount() From a43f25ea260613d6a6f8510c55c24b384c4ceb31 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 3 Apr 2025 09:09:38 +0300 Subject: [PATCH 03/18] chore: store subscribe --- packages/vue-form/src/useField.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vue-form/src/useField.ts b/packages/vue-form/src/useField.ts index 23f603249..a2dca69a2 100644 --- a/packages/vue-form/src/useField.ts +++ b/packages/vue-form/src/useField.ts @@ -564,7 +564,6 @@ export function useField< let cleanup = NOOP const state = shallowRef(api!.store.state) const unsubscribeStore = api!.store.subscribe(() => { - if (shallow(state.value, api!.store.state)) return triggerRef(state) }) From a2f4b14669472bed2381b347871b39a99b793109 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 3 Apr 2025 09:24:17 +0300 Subject: [PATCH 04/18] chore: useStore shallowRef --- packages/vue-form/src/useForm.ts | 33 ++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/vue-form/src/useForm.ts b/packages/vue-form/src/useForm.ts index 230e15d65..028e6a180 100644 --- a/packages/vue-form/src/useForm.ts +++ b/packages/vue-form/src/useForm.ts @@ -1,6 +1,12 @@ -import { useStore } from '@tanstack/vue-store' import { FormApi } from '@tanstack/form-core' -import { defineComponent, h, onMounted, onScopeDispose, watchEffect } from 'vue' +import { + defineComponent, + h, + onMounted, + onScopeDispose, + shallowRef, + watchEffect, +} from 'vue' import { Field, useField } from './useField' import { NOOP } from './utils' import type { FieldComponent, UseField } from './useField' @@ -263,13 +269,24 @@ export function useForm< extendedApi.useField = (props) => { return useField(() => ({ ...props(), form: api })) as never } - extendedApi.useStore = (selector) => { - return useStore(api.store as never, selector as never) as never + extendedApi.useStore = (selector = (v) => v as never) => { + const state = shallowRef(selector(api!.store.state)) + const cleanup = api!.store.subscribe(() => { + state.value = selector(api!.store.state) + }) + onScopeDispose(cleanup) + + return state as never } extendedApi.Subscribe = defineComponent( - (props, context) => { - const data = useStore(api.store as never, props.selector as never) - return () => context.slots.default!(data.value) + (props, { slots }) => { + const state = shallowRef(props.selector(api!.store.state)) + const cleanup = api!.store.subscribe(() => { + state.value = props.selector(api!.store.state) + }) + onScopeDispose(cleanup) + + return () => slots.default!(state.value) }, { name: 'Subscribe', @@ -277,7 +294,7 @@ export function useForm< props: { selector: { type: Function, - default: undefined, + default: (v) => v, }, }, }, From b68f55c65ec7b5b2df85816c62a8b94dc515e0b6 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 3 Apr 2025 09:26:30 +0300 Subject: [PATCH 05/18] chore: Field props --- packages/vue-form/src/useForm.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/vue-form/src/useForm.ts b/packages/vue-form/src/useForm.ts index 028e6a180..ed902f424 100644 --- a/packages/vue-form/src/useForm.ts +++ b/packages/vue-form/src/useForm.ts @@ -252,13 +252,9 @@ export function useForm< > = api! as never extendedApi.Field = defineComponent( - (props, context) => { + (_, context) => { return () => - h( - Field as never, - { ...props, ...context.attrs, form: api }, - context.slots, - ) + h(Field as never, { ...context.attrs, form: api }, context.slots) }, { name: 'APIField', From 26577e02fb59ba4179958d24f620b941c387d77d Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 3 Apr 2025 09:29:30 +0300 Subject: [PATCH 06/18] fix: update api --- packages/vue-form/src/useForm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-form/src/useForm.ts b/packages/vue-form/src/useForm.ts index ed902f424..6c1407eb9 100644 --- a/packages/vue-form/src/useForm.ts +++ b/packages/vue-form/src/useForm.ts @@ -234,7 +234,7 @@ export function useForm< watchEffect(() => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!api) api = new FormApi(opts?.()) - api.update(opts?.()) + else api.update(opts?.()) }) const extendedApi: typeof api & From 649f2a12c680728958dca42885c03b2b87dc01e5 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 3 Apr 2025 10:33:34 +0300 Subject: [PATCH 07/18] chore: form api --- packages/vue-form/src/useForm.ts | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/vue-form/src/useForm.ts b/packages/vue-form/src/useForm.ts index 6c1407eb9..9879d2577 100644 --- a/packages/vue-form/src/useForm.ts +++ b/packages/vue-form/src/useForm.ts @@ -229,15 +229,7 @@ export function useForm< TFormOnSubmitAsync, TFormOnServer, TSubmitMeta - > - - watchEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!api) api = new FormApi(opts?.()) - else api.update(opts?.()) - }) - - const extendedApi: typeof api & + > & VueFormApi< TParentData, TFormOnMount, @@ -249,9 +241,15 @@ export function useForm< TFormOnSubmitAsync, TFormOnServer, TSubmitMeta - > = api! as never + > + + watchEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!api) api = new FormApi(opts?.()) as never + else api.update(opts?.()) + }) - extendedApi.Field = defineComponent( + api!.Field = defineComponent( (_, context) => { return () => h(Field as never, { ...context.attrs, form: api }, context.slots) @@ -262,10 +260,10 @@ export function useForm< }, ) as never - extendedApi.useField = (props) => { + api!.useField = (props) => { return useField(() => ({ ...props(), form: api })) as never } - extendedApi.useStore = (selector = (v) => v as never) => { + api!.useStore = (selector = (v) => v as never) => { const state = shallowRef(selector(api!.store.state)) const cleanup = api!.store.subscribe(() => { state.value = selector(api!.store.state) @@ -274,7 +272,7 @@ export function useForm< return state as never } - extendedApi.Subscribe = defineComponent( + api!.Subscribe = defineComponent( (props, { slots }) => { const state = shallowRef(props.selector(api!.store.state)) const cleanup = api!.store.subscribe(() => { @@ -298,9 +296,9 @@ export function useForm< let cleanup = NOOP onMounted(() => { - cleanup = extendedApi.mount() + cleanup = api!.mount() }) onScopeDispose(cleanup) - return extendedApi + return api! } From 86e61d3ac2b3d67afbf2e85424823f5e72d93ef3 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 3 Apr 2025 11:27:15 +0300 Subject: [PATCH 08/18] chore: inline noop fn --- packages/vue-form/src/useField.ts | 7 +++---- packages/vue-form/src/useForm.ts | 3 +-- packages/vue-form/src/utils.ts | 1 - 3 files changed, 4 insertions(+), 7 deletions(-) delete mode 100644 packages/vue-form/src/utils.ts diff --git a/packages/vue-form/src/useField.ts b/packages/vue-form/src/useField.ts index a2dca69a2..25530d72c 100644 --- a/packages/vue-form/src/useField.ts +++ b/packages/vue-form/src/useField.ts @@ -3,12 +3,11 @@ import { defineComponent, onBeforeUnmount, onMounted, + onScopeDispose, shallowRef, triggerRef, watchEffect, } from 'vue' -import { shallow } from '@tanstack/vue-store' -import { NOOP } from './utils' import type { DeepKeys, DeepValue, @@ -561,17 +560,17 @@ export function useField< } }) - let cleanup = NOOP const state = shallowRef(api!.store.state) const unsubscribeStore = api!.store.subscribe(() => { triggerRef(state) }) + let cleanup = () => {} onMounted(() => { cleanup = api.mount() }) - onBeforeUnmount(() => { + onScopeDispose(() => { cleanup() unsubscribeStore() }) diff --git a/packages/vue-form/src/useForm.ts b/packages/vue-form/src/useForm.ts index 9879d2577..44709d67a 100644 --- a/packages/vue-form/src/useForm.ts +++ b/packages/vue-form/src/useForm.ts @@ -8,7 +8,6 @@ import { watchEffect, } from 'vue' import { Field, useField } from './useField' -import { NOOP } from './utils' import type { FieldComponent, UseField } from './useField' import type { DefineSetupFnComponent, @@ -294,7 +293,7 @@ export function useForm< }, ) as never - let cleanup = NOOP + let cleanup = () => {} onMounted(() => { cleanup = api!.mount() }) diff --git a/packages/vue-form/src/utils.ts b/packages/vue-form/src/utils.ts deleted file mode 100644 index 8e6667278..000000000 --- a/packages/vue-form/src/utils.ts +++ /dev/null @@ -1 +0,0 @@ -export const NOOP = () => {} From 493841e7d2fea33e25d29d1f935f5df18c0ab03a Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Fri, 4 Apr 2025 20:01:52 +0300 Subject: [PATCH 09/18] refactor: use function component --- packages/vue-form/src/useForm.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/vue-form/src/useForm.ts b/packages/vue-form/src/useForm.ts index 44709d67a..feaf7f019 100644 --- a/packages/vue-form/src/useForm.ts +++ b/packages/vue-form/src/useForm.ts @@ -13,6 +13,7 @@ import type { DefineSetupFnComponent, EmitsOptions, EmitsToProps, + FunctionalComponent, PublicProps, Ref, SlotsType, @@ -248,16 +249,10 @@ export function useForm< else api.update(opts?.()) }) - api!.Field = defineComponent( - (_, context) => { - return () => - h(Field as never, { ...context.attrs, form: api }, context.slots) - }, - { - name: 'APIField', - inheritAttrs: false, - }, - ) as never + const APIField: FunctionalComponent = (_, { attrs, slots }) => + h(Field as never, { ...attrs, form: api }, slots) + + api!.Field = APIField as never api!.useField = (props) => { return useField(() => ({ ...props(), form: api })) as never From 5de6a1f78db7cbf9ecde6cb7606f6d79bee983ad Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Fri, 4 Apr 2025 20:08:50 +0300 Subject: [PATCH 10/18] docs: update --- docs/framework/vue/guides/arrays.md | 8 ++++---- .../vue/guides/async-initial-values.md | 17 ++++++----------- docs/framework/vue/guides/basic-concepts.md | 12 ++++++------ docs/framework/vue/guides/linked-fields.md | 4 ++-- docs/framework/vue/guides/listeners.md | 4 ++-- docs/framework/vue/guides/validation.md | 8 ++++---- docs/framework/vue/quick-start.md | 4 ++-- 7 files changed, 26 insertions(+), 31 deletions(-) diff --git a/docs/framework/vue/guides/arrays.md b/docs/framework/vue/guides/arrays.md index 81827a2ff..11803d2cc 100644 --- a/docs/framework/vue/guides/arrays.md +++ b/docs/framework/vue/guides/arrays.md @@ -14,12 +14,12 @@ with [`Index` from `solid-js`](https://www.solidjs.com/tutorial/flow_index):