From 8e90e4fe668902ac7a0bfae8c4bd58b15b918328 Mon Sep 17 00:00:00 2001 From: Aadit Olkar <63646058+aadito123@users.noreply.github.com> Date: Sun, 3 Dec 2023 20:26:34 +0800 Subject: [PATCH] fix: Vue useStore typings are now correct in JSX usage * initial attempt to add back form validation * uncomment tests * fixed form validation not running * onChange + onBlur * feat: mount method on FormApi * fix solid-form test case * fix checkLatest * add onMount logic + test * react form validation tests * solid tests * prettier * starting vue tests * vue test struggles * chore: remove Vue 2 compat * test: refactor of Vue tests * fix: Vue's typings for useStore are now correct * chore: fix last failing Vue test --------- Co-authored-by: Corbin Crutchley --- examples/vue/simple/vite.config.ts | 2 +- examples/vue/valibot/vite.config.ts | 2 +- examples/vue/yup/vite.config.ts | 2 +- examples/vue/zod/vite.config.ts | 2 +- .../react-form/src/tests/useForm.test.tsx | 298 ++++++++++++- .../solid-form/src/tests/createForm.test.tsx | 321 +++++++++++++- packages/vue-form/package.json | 21 +- packages/vue-form/src/formContext.ts | 2 +- packages/vue-form/src/tests/useField.test.tsx | 10 +- packages/vue-form/src/tests/useForm.test.tsx | 414 ++++++++++++++++-- packages/vue-form/src/useField.tsx | 4 +- packages/vue-form/src/useForm.tsx | 5 +- packages/vue-form/test-setup.ts | 8 - packages/vue-form/vitest.config.ts | 2 +- pnpm-lock.yaml | 48 +- 15 files changed, 1024 insertions(+), 117 deletions(-) delete mode 100644 packages/vue-form/test-setup.ts diff --git a/examples/vue/simple/vite.config.ts b/examples/vue/simple/vite.config.ts index 75eb5ed95..52469465d 100644 --- a/examples/vue/simple/vite.config.ts +++ b/examples/vue/simple/vite.config.ts @@ -5,6 +5,6 @@ import createVuePlugin from '@vitejs/plugin-vue' export default defineConfig({ plugins: [createVuePlugin()], optimizeDeps: { - exclude: ['@tanstack/vue-form', 'vue-demi'], + exclude: ['@tanstack/vue-form'], }, }) diff --git a/examples/vue/valibot/vite.config.ts b/examples/vue/valibot/vite.config.ts index 75eb5ed95..52469465d 100644 --- a/examples/vue/valibot/vite.config.ts +++ b/examples/vue/valibot/vite.config.ts @@ -5,6 +5,6 @@ import createVuePlugin from '@vitejs/plugin-vue' export default defineConfig({ plugins: [createVuePlugin()], optimizeDeps: { - exclude: ['@tanstack/vue-form', 'vue-demi'], + exclude: ['@tanstack/vue-form'], }, }) diff --git a/examples/vue/yup/vite.config.ts b/examples/vue/yup/vite.config.ts index 75eb5ed95..52469465d 100644 --- a/examples/vue/yup/vite.config.ts +++ b/examples/vue/yup/vite.config.ts @@ -5,6 +5,6 @@ import createVuePlugin from '@vitejs/plugin-vue' export default defineConfig({ plugins: [createVuePlugin()], optimizeDeps: { - exclude: ['@tanstack/vue-form', 'vue-demi'], + exclude: ['@tanstack/vue-form'], }, }) diff --git a/examples/vue/zod/vite.config.ts b/examples/vue/zod/vite.config.ts index 75eb5ed95..52469465d 100644 --- a/examples/vue/zod/vite.config.ts +++ b/examples/vue/zod/vite.config.ts @@ -5,6 +5,6 @@ import createVuePlugin from '@vitejs/plugin-vue' export default defineConfig({ plugins: [createVuePlugin()], optimizeDeps: { - exclude: ['@tanstack/vue-form', 'vue-demi'], + exclude: ['@tanstack/vue-form'], }, }) diff --git a/packages/react-form/src/tests/useForm.test.tsx b/packages/react-form/src/tests/useForm.test.tsx index c6e8066db..f3dca6a5f 100644 --- a/packages/react-form/src/tests/useForm.test.tsx +++ b/packages/react-form/src/tests/useForm.test.tsx @@ -1,9 +1,10 @@ /// +import '@testing-library/jest-dom' import { render, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' import * as React from 'react' import { createFormFactory, useForm } from '..' +import { sleep } from './utils' const user = userEvent.setup() @@ -173,7 +174,6 @@ describe('useForm', () => { return error }, }) - return ( { await waitFor(() => getByText(error)) expect(getByText(error)).toBeInTheDocument() }) + + it('should not validate on change if isTouched is false', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.useForm({ + onChange: (value) => (value.firstName === 'other' ? error : undefined), + }) + + const errors = form.useStore((s) => s.errors) + return ( + + ( +
+ field.setValue(e.target.value)} + /> +

{errors}

+
+ )} + /> +
+ ) + } + + const { getByTestId, queryByText } = render() + const input = getByTestId('fieldinput') + await user.type(input, 'other') + expect(queryByText(error)).not.toBeInTheDocument() + }) + + it('should validate on change if isTouched is true', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.useForm({ + onChange: (value) => (value.firstName === 'other' ? error : undefined), + }) + const errors = form.useStore((s) => s.errorMap) + return ( + + ( +
+ field.handleChange(e.target.value)} + /> +

{errors.onChange}

+
+ )} + /> +
+ ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate on change and on blur', async () => { + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + }, + onChange: (vals) => { + if (vals.firstName === 'other') return onChangeError + return undefined + }, + onBlur: (vals) => { + if (vals.firstName === 'other') return onBlurError + return undefined + }, + }) + + const errors = form.useStore((s) => s.errorMap) + return ( + + ( +
+ field.handleChange(e.target.value)} + /> +

{errors.onChange}

+

{errors.onBlur}

+
+ )} + /> +
+ ) + } + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(onChangeError)).toBeInTheDocument() + await user.click(document.body) + expect(queryByText(onBlurError)).toBeInTheDocument() + }) + + it('should validate async on change', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.useForm({ + onChangeAsync: async () => { + await sleep(10) + return error + }, + }) + const errors = form.useStore((s) => s.errorMap) + return ( + + ( +
+ field.handleChange(e.target.value)} + /> +

{errors.onChange}

+
+ )} + /> +
+ ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate async on change and async on blur', async () => { + type Person = { + firstName: string + lastName: string + } + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.useForm({ + onChangeAsync: async () => { + await sleep(10) + return onChangeError + }, + onBlurAsync: async () => { + await sleep(10) + return onBlurError + }, + }) + const errors = form.useStore((s) => s.errorMap) + + return ( + + ( +
+ field.handleChange(e.target.value)} + /> +

{errors.onChange}

+

{errors.onBlur}

+
+ )} + /> +
+ ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(onChangeError)) + expect(getByText(onChangeError)).toBeInTheDocument() + await user.click(document.body) + await waitFor(() => getByText(onBlurError)) + expect(getByText(onBlurError)).toBeInTheDocument() + }) + + it('should validate async on change with debounce', async () => { + type Person = { + firstName: string + lastName: string + } + const mockFn = vi.fn() + const error = 'Please enter a different value' + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.useForm({ + onChangeAsyncDebounceMs: 100, + onChangeAsync: async () => { + mockFn() + await sleep(10) + return error + }, + }) + const errors = form.useStore((s) => s.errors) + + return ( + + ( +
+ field.handleChange(e.target.value)} + /> +

{errors}

+
+ )} + /> +
+ ) + } + + const { getByTestId, getByText } = render() + const input = getByTestId('fieldinput') + await user.type(input, 'other') + // mockFn will have been called 5 times without onChangeAsyncDebounceMs + expect(mockFn).toHaveBeenCalledTimes(0) + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) }) diff --git a/packages/solid-form/src/tests/createForm.test.tsx b/packages/solid-form/src/tests/createForm.test.tsx index 77e30316a..898b760b8 100644 --- a/packages/solid-form/src/tests/createForm.test.tsx +++ b/packages/solid-form/src/tests/createForm.test.tsx @@ -1,8 +1,10 @@ import { render, screen, waitFor } from '@solidjs/testing-library' import '@testing-library/jest-dom' import userEvent from '@testing-library/user-event' -import { createFormFactory, createForm } from '..' -import { Show, createSignal } from 'solid-js' +import { Show, createSignal, onCleanup } from 'solid-js' +import { createForm, createFormFactory } from '..' +import { sleep } from './utils' +import type { ValidationErrorMap } from '@tanstack/form-core' const user = userEvent.setup() @@ -147,4 +149,319 @@ describe('createForm', () => { await user.click(getByText('Mount form')) expect(formMounted()).toBe(true) }) + + it('should not validate on change if isTouched is false', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.createForm(() => ({ + onChange: (value) => + value.firstName.includes('other') ? error : undefined, + })) + + return ( + + ( +
+ field().setValue(e.currentTarget.value)} + /> +

{form.useStore((s) => s.errors)}

+
+ )} + /> +
+ ) + } + + const { getByTestId, queryByText } = render(() => ) + const input = getByTestId('fieldinput') + await user.type(input, 'other') + expect(queryByText(error)).not.toBeInTheDocument() + }) + + it('should validate on change if isTouched is true', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.createForm(() => ({ + onChange: (value) => + value.firstName.includes('other') ? error : undefined, + })) + + const [errors, setErrors] = createSignal() + onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) + + return ( + + { + return ( +
+ field().setValue(e.currentTarget.value)} + /> +

{errors()?.onChange}

+
+ ) + }} + /> +
+ ) + } + + const { getByTestId, getByText, queryByText } = render(() => ) + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate on change and on blur', async () => { + type Person = { + firstName: string + lastName: string + } + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.createForm(() => ({ + onChange: (value) => + value.firstName.includes('other') ? onChangeError : undefined, + onBlur: (value) => + value.firstName.includes('other') ? onBlurError : undefined, + })) + + const [errors, setErrors] = createSignal() + onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) + + return ( + + ( +
+ field().handleChange(e.currentTarget.value)} + /> +

{errors()?.onChange}

+

{errors()?.onBlur}

+
+ )} + /> +
+ ) + } + + const { getByTestId, getByText, queryByText } = render(() => ) + const input = getByTestId('fieldinput') + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(onChangeError)).toBeInTheDocument() + // @ts-expect-error unsure why the 'vitest/globals' in tsconfig doesnt work here + await user.click(document.body) + expect(queryByText(onBlurError)).toBeInTheDocument() + }) + + it('should validate async on change', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.createForm(() => ({ + onChangeAsync: async () => { + await sleep(10) + return error + }, + })) + + const [errors, setErrors] = createSignal() + onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) + + return ( + + ( +
+ field().handleChange(e.currentTarget.value)} + /> +

{errors()?.onChange}

+
+ )} + /> +
+ ) + } + + const { getByTestId, getByText, queryByText } = render(() => ) + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate async on change and async on blur', async () => { + type Person = { + firstName: string + lastName: string + } + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.createForm(() => ({ + async onChangeAsync() { + await sleep(10) + return onChangeError + }, + async onBlurAsync() { + await sleep(10) + return onBlurError + }, + })) + + const [errors, setErrors] = createSignal() + onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) + + return ( + + ( +
+ field().handleChange(e.currentTarget.value)} + /> +

{errors()?.onChange}

+

{errors()?.onBlur}

+
+ )} + /> +
+ ) + } + + const { getByTestId, getByText, queryByText } = render(() => ) + const input = getByTestId('fieldinput') + + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(onChangeError)) + expect(getByText(onChangeError)).toBeInTheDocument() + // @ts-expect-error unsure why the 'vitest/globals' in tsconfig doesnt work here + await user.click(document.body) + await waitFor(() => getByText(onBlurError)) + expect(getByText(onBlurError)).toBeInTheDocument() + }) + + it('should validate async on change with debounce', async () => { + type Person = { + firstName: string + lastName: string + } + const mockFn = vi.fn() + const error = 'Please enter a different value' + const formFactory = createFormFactory() + + function Comp() { + const form = formFactory.createForm(() => ({ + onChangeAsyncDebounceMs: 100, + onChangeAsync: async () => { + mockFn() + await sleep(10) + return error + }, + })) + + const [errors, setErrors] = createSignal() + onCleanup( + form.store.subscribe(() => + setErrors(form.state.errorMap.onChange || ''), + ), + ) + + return ( + + ( +
+ field().handleChange(e.currentTarget.value)} + /> +

{errors()}

+
+ )} + /> +
+ ) + } + + const { getByTestId, getByText } = render(() => ) + const input = getByTestId('fieldinput') + await user.type(input, 'other') + // mockFn will have been called 5 times without onChangeAsyncDebounceMs + expect(mockFn).toHaveBeenCalledTimes(0) + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) }) diff --git a/packages/vue-form/package.json b/packages/vue-form/package.json index eea3e78eb..a5ae6f1f2 100644 --- a/packages/vue-form/package.json +++ b/packages/vue-form/package.json @@ -33,10 +33,7 @@ "test:eslint": "eslint --ext .ts,.tsx ./src", "test:types": "tsc", "fixme:test:lib": "pnpm run test:2 && pnpm run test:2.7 && pnpm run test:3", - "test:lib": "pnpm run test:3", - "test:2": "vue-demi-switch 2 vue2 && vitest", - "test:2.7": "vue-demi-switch 2.7 vue2.7 && vitest", - "test:3": "vue-demi-switch 3 && vitest", + "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", "build": "tsup" @@ -57,22 +54,12 @@ "dependencies": { "@tanstack/form-core": "workspace:*", "@tanstack/store": "0.1.3", - "@tanstack/vue-store": "0.1.3", - "vue-demi": "^0.14.6" + "@tanstack/vue-store": "0.1.3" }, "devDependencies": { - "@vue/composition-api": "1.7.2", - "vue": "^3.3.4", - "vue2": "npm:vue@2.6", - "vue2.7": "npm:vue@2.7" + "vue": "^3.3.4" }, "peerDependencies": { - "@vue/composition-api": "^1.1.2", - "vue": "^2.5.0 || ^3.0.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "vue": "^3.3.0" } } diff --git a/packages/vue-form/src/formContext.ts b/packages/vue-form/src/formContext.ts index 74044807b..4faa604f9 100644 --- a/packages/vue-form/src/formContext.ts +++ b/packages/vue-form/src/formContext.ts @@ -1,5 +1,5 @@ import type { FormApi } from '@tanstack/form-core' -import { inject, provide } from 'vue-demi' +import { inject, provide } from 'vue' export type FormContext = { formApi: FormApi diff --git a/packages/vue-form/src/tests/useField.test.tsx b/packages/vue-form/src/tests/useField.test.tsx index 1e0655723..caa66b59a 100644 --- a/packages/vue-form/src/tests/useField.test.tsx +++ b/packages/vue-form/src/tests/useField.test.tsx @@ -1,15 +1,9 @@ /// -import { h, defineComponent } from 'vue-demi' +import { h, defineComponent } from 'vue' import { render, waitFor } from '@testing-library/vue' import '@testing-library/jest-dom' -import { - createFormFactory, - type FieldApi, - provideFormContext, - useForm, -} from '../index' +import { createFormFactory, type FieldApi, provideFormContext } from '../index' import userEvent from '@testing-library/user-event' -import * as React from 'react' import { sleep } from './utils' const user = userEvent.setup() diff --git a/packages/vue-form/src/tests/useForm.test.tsx b/packages/vue-form/src/tests/useForm.test.tsx index 757f23bd1..67d17029c 100644 --- a/packages/vue-form/src/tests/useForm.test.tsx +++ b/packages/vue-form/src/tests/useForm.test.tsx @@ -1,25 +1,27 @@ /// -import { h, defineComponent, ref } from 'vue-demi' -import { render, waitFor } from '@testing-library/vue' import '@testing-library/jest-dom' +import userEvent from '@testing-library/user-event' +import { render, waitFor } from '@testing-library/vue' +import { h, defineComponent, ref } from 'vue' import { + ValidationError, createFormFactory, - type FieldApi, provideFormContext, useForm, -} from '../index' -import userEvent from '@testing-library/user-event' -import * as React from 'react' +} from '..' +import { sleep } from './utils' + +import type { FieldApi, ValidationErrorMap } from '..' const user = userEvent.setup() +type Person = { + firstName: string + lastName: string +} + describe('useForm', () => { it('preserved field state', async () => { - type Person = { - firstName: string - lastName: string - } - const formFactory = createFormFactory() const Comp = defineComponent(() => { @@ -47,7 +49,7 @@ describe('useForm', () => { ) }) - const { getByTestId, queryByText } = render(Comp) + const { getByTestId, queryByText } = render() const input = getByTestId('fieldinput') expect(queryByText('FirstName')).not.toBeInTheDocument() await user.type(input, 'FirstName') @@ -55,11 +57,6 @@ describe('useForm', () => { }) it('should allow default values to be set', async () => { - type Person = { - firstName: string - lastName: string - } - const formFactory = createFormFactory() const Comp = defineComponent(() => { @@ -82,7 +79,7 @@ describe('useForm', () => { ) }) - const { findByText, queryByText } = render(Comp) + const { findByText, queryByText } = render() expect(await findByText('FirstName')).toBeInTheDocument() expect(queryByText('LastName')).not.toBeInTheDocument() }) @@ -102,18 +99,18 @@ describe('useForm', () => { form.provideFormContext() return () => ( - +
{({ field, }: { - field: FieldApi<{ firstName: string }, 'firstName', never, never> + field: FieldApi }) => { return ( + onInput={(e) => field.handleChange((e.target as HTMLInputElement).value) } placeholder={'First name'} @@ -125,11 +122,11 @@ describe('useForm', () => { {submittedData.value && (

Submitted data: {submittedData.value.firstName}

)} - +
) }) - const { findByPlaceholderText, getByText } = render(Comp) + const { findByPlaceholderText, getByText } = render() const input = await findByPlaceholderText('First name') await user.clear(input) await user.type(input, 'OtherName') @@ -153,20 +150,385 @@ describe('useForm', () => { return undefined }, }) + form.provideFormContext() return () => mountForm.value ? ( - +

{formMounted.value ? 'Form mounted' : 'Not mounted'}

- +
) : ( ) }) - const { getByText, findByText } = render(Comp) + const { getByText, findByText } = render() await user.click(getByText('Mount form')) expect(await findByText('Form mounted')).toBeInTheDocument() }) + + it('should validate async on change for the form', async () => { + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + const Comp = defineComponent(() => { + const form = formFactory.useForm({ + onChange() { + return error + }, + }) + + form.provideFormContext() + + return () => ( +
+ + {({ + field, + }: { + field: FieldApi + }) => ( + + field.handleChange((e.target as HTMLInputElement).value) + } + /> + )} + + state.errorMap}> + {(errorMap: ValidationErrorMap) =>

{errorMap.onChange}

} +
+
+ ) + }) + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + it('should not validate on change if isTouched is false', async () => { + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + const Comp = defineComponent(() => { + const form = formFactory.useForm({ + onChange: (value) => (value.firstName === 'other' ? error : undefined), + }) + + const errors = form.useStore((s) => s.errors) + + form.provideFormContext() + + return () => ( +
+ + {({ + field, + }: { + field: FieldApi + }) => ( +
+ + field.setValue((e.target as HTMLInputElement).value) + } + /> +
+ )} +
+

{errors}

+
+ ) + }) + + const { getByTestId, queryByText } = render() + const input = getByTestId('fieldinput') + await user.type(input, 'other') + expect(queryByText(error)).not.toBeInTheDocument() + }) + + it('should validate on change if isTouched is true', async () => { + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + const Comp = defineComponent(() => { + const form = formFactory.useForm({ + onChange: (value) => (value.firstName === 'other' ? error : undefined), + }) + + const errors = form.useStore((s) => s.errorMap) + + form.provideFormContext() + + return () => ( +
+ + {({ + field, + }: { + field: FieldApi + }) => ( +
+ + field.handleChange((e.target as HTMLInputElement).value) + } + /> +

{errors.value.onChange}

+
+ )} +
+
+ ) + }) + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate on change and on blur', async () => { + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + const Comp = defineComponent(() => { + const form = useForm({ + defaultValues: { + firstName: '', + }, + onChange: (vals) => { + if (vals.firstName === 'other') return onChangeError + return undefined + }, + onBlur: (vals) => { + if (vals.firstName === 'other') return onBlurError + return undefined + }, + }) + + const errors = form.useStore((s) => s.errorMap) + + form.provideFormContext() + + return () => ( +
+ + {({ + field, + }: { + field: FieldApi + }) => ( +
+ + field.handleChange((e.target as HTMLInputElement).value) + } + /> +

{errors.value.onChange}

+

{errors.value.onBlur}

+
+ )} +
+
+ ) + }) + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(onChangeError)).toBeInTheDocument() + await user.click(document.body) + expect(queryByText(onBlurError)).toBeInTheDocument() + }) + + it('should validate async on change', async () => { + const error = 'Please enter a different value' + + const formFactory = createFormFactory() + + const Comp = defineComponent(() => { + const form = formFactory.useForm({ + onChangeAsync: async () => { + await sleep(10) + return error + }, + }) + + const errors = form.useStore((s) => s.errorMap) + + form.provideFormContext() + + return () => ( +
+ + {({ + field, + }: { + field: FieldApi + }) => ( +
+ + field.handleChange((e.target as HTMLInputElement).value) + } + /> +

{errors.value.onChange}

+
+ )} +
+
+ ) + }) + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate async on change and async on blur', async () => { + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + const formFactory = createFormFactory() + + const Comp = defineComponent(() => { + const form = formFactory.useForm({ + onChangeAsync: async () => { + await sleep(10) + return onChangeError + }, + onBlurAsync: async () => { + await sleep(10) + return onBlurError + }, + }) + const errors = form.useStore((s) => s.errorMap) + + form.provideFormContext() + + return () => ( +
+ + {({ + field, + }: { + field: FieldApi + }) => ( +
+ + field.handleChange((e.target as HTMLInputElement).value) + } + /> +

{errors.value.onChange}

+

{errors.value.onBlur}

+
+ )} +
+
+ ) + }) + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(onChangeError)) + expect(getByText(onChangeError)).toBeInTheDocument() + await user.click(document.body) + await waitFor(() => getByText(onBlurError)) + expect(getByText(onBlurError)).toBeInTheDocument() + }) + + it('should validate async on change with debounce', async () => { + const mockFn = vi.fn() + const error = 'Please enter a different value' + const formFactory = createFormFactory() + + const Comp = defineComponent(() => { + const form = formFactory.useForm({ + onChangeAsyncDebounceMs: 100, + onChangeAsync: async () => { + mockFn() + await sleep(10) + return error + }, + }) + const errors = form.useStore((s) => s.errors) + + form.provideFormContext() + + return () => ( +
+ + {({ + field, + }: { + field: FieldApi + }) => ( +
+ + field.handleChange((e.target as HTMLInputElement).value) + } + /> +

{errors.value.join(',')}

+
+ )} +
+
+ ) + }) + + const { getByTestId, getByText } = render() + const input = getByTestId('fieldinput') + await user.type(input, 'other') + // mockFn will have been called 5 times without onChangeAsyncDebounceMs + expect(mockFn).toHaveBeenCalledTimes(0) + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) }) diff --git a/packages/vue-form/src/useField.tsx b/packages/vue-form/src/useField.tsx index ee49b3d27..27dbc7198 100644 --- a/packages/vue-form/src/useField.tsx +++ b/packages/vue-form/src/useField.tsx @@ -1,8 +1,8 @@ import { FieldApi } from '@tanstack/form-core' import type { DeepKeys, DeepValue, Narrow } from '@tanstack/form-core' import { useStore } from '@tanstack/vue-store' -import { defineComponent, onMounted, onUnmounted, watch } from 'vue-demi' -import type { SlotsType, SetupContext, Ref } from 'vue-demi' +import { defineComponent, onMounted, onUnmounted, watch } from 'vue' +import type { SlotsType, SetupContext, Ref } from 'vue' import { provideFormContext, useFormContext } from './formContext' import type { UseFieldOptions } from './types' diff --git a/packages/vue-form/src/useForm.tsx b/packages/vue-form/src/useForm.tsx index bb8c0d746..50b267eed 100644 --- a/packages/vue-form/src/useForm.tsx +++ b/packages/vue-form/src/useForm.tsx @@ -6,9 +6,10 @@ import { type EmitsOptions, type SlotsType, type SetupContext, + type Ref, defineComponent, onMounted, -} from 'vue-demi' +} from 'vue' declare module '@tanstack/form-core' { // eslint-disable-next-line no-shadow @@ -19,7 +20,7 @@ declare module '@tanstack/form-core' { useField: UseField useStore: >>( selector?: (state: NoInfer>) => TSelected, - ) => TSelected + ) => Readonly> Subscribe: >>( props: { selector?: (state: NoInfer>) => TSelected diff --git a/packages/vue-form/test-setup.ts b/packages/vue-form/test-setup.ts deleted file mode 100644 index 34ecaae3b..000000000 --- a/packages/vue-form/test-setup.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Vue from 'vue2' -Vue.config.productionTip = false -Vue.config.devtools = false - -// Hide annoying console warnings for Vue2 -import Vue27 from 'vue2.7' -Vue27.config.productionTip = false -Vue27.config.devtools = false diff --git a/packages/vue-form/vitest.config.ts b/packages/vue-form/vitest.config.ts index 79d2352a3..5eb5bd583 100644 --- a/packages/vue-form/vitest.config.ts +++ b/packages/vue-form/vitest.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ watch: false, environment: 'jsdom', globals: true, - setupFiles: ['test-setup.ts'], + setupFiles: [], coverage: { provider: 'istanbul' }, }, esbuild: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6caa1a70..28e7818f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -798,23 +798,11 @@ importers: version: 0.1.3 '@tanstack/vue-store': specifier: 0.1.3 - version: 0.1.3(@vue/composition-api@1.7.2)(vue@3.3.4) - vue-demi: - specifier: ^0.14.6 - version: 0.14.6(@vue/composition-api@1.7.2)(vue@3.3.4) + version: 0.1.3(vue@3.3.4) devDependencies: - '@vue/composition-api': - specifier: 1.7.2 - version: 1.7.2(vue@3.3.4) vue: specifier: ^3.3.4 version: 3.3.4 - vue2: - specifier: npm:vue@2.6 - version: /vue@2.6.14 - vue2.7: - specifier: npm:vue@2.7 - version: /vue@2.7.14 packages/yup-form-adapter: dependencies: @@ -3182,7 +3170,7 @@ packages: resolution: {integrity: sha512-GnolmC8Fr4mvsHE1fGQmR3Nm0eBO3KnZjDU0a+P3TeQNM/dDscFGxtA7p31NplQNW3KwBw4t1RVFmz0VeKLxcw==} dev: false - /@tanstack/vue-store@0.1.3(@vue/composition-api@1.7.2)(vue@3.3.4): + /@tanstack/vue-store@0.1.3(vue@3.3.4): resolution: {integrity: sha512-vLG2t0N0nagS2YrCDtTGJiftBDWadX0M7vSkIjfl+I1SaeKDvhg+HbRxt7Vwc5iLcwHljHAIlf7Ak5o4q0Brhg==} peerDependencies: '@vue/composition-api': ^1.2.1 @@ -3192,9 +3180,8 @@ packages: optional: true dependencies: '@tanstack/store': 0.1.3 - '@vue/composition-api': 1.7.2(vue@3.3.4) vue: 3.3.4 - vue-demi: 0.14.6(@vue/composition-api@1.7.2)(vue@3.3.4) + vue-demi: 0.14.6(vue@3.3.4) dev: false /@testing-library/dom@9.3.1: @@ -3749,14 +3736,6 @@ packages: '@vue/compiler-core': 3.3.4 '@vue/shared': 3.3.4 - /@vue/compiler-sfc@2.7.14: - resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==} - dependencies: - '@babel/parser': 7.22.13 - postcss: 8.4.28 - source-map: 0.6.1 - dev: true - /@vue/compiler-sfc@3.3.4: resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} dependencies: @@ -3777,13 +3756,6 @@ packages: '@vue/compiler-dom': 3.3.4 '@vue/shared': 3.3.4 - /@vue/composition-api@1.7.2(vue@3.3.4): - resolution: {integrity: sha512-M8jm9J/laYrYT02665HkZ5l2fWTK4dcVg3BsDHm/pfz+MjDYwX+9FUaZyGwEyXEDonQYRCo0H7aLgdklcIELjw==} - peerDependencies: - vue: '>= 2.5 < 2.7' - dependencies: - vue: 3.3.4 - /@vue/language-core@1.8.10(typescript@5.2.2): resolution: {integrity: sha512-db8PtM4ZZr7SYNH30XpKxUYnUBYaTvcuJ4c2whKK04fuAjbtjAIZ2al5GzGEfUlesmvkpgdbiSviRXUxgD9Omw==} peerDependencies: @@ -10473,7 +10445,7 @@ packages: resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==} dev: true - /vue-demi@0.14.6(@vue/composition-api@1.7.2)(vue@3.3.4): + /vue-demi@0.14.6(vue@3.3.4): resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} engines: {node: '>=12'} hasBin: true @@ -10485,7 +10457,6 @@ packages: '@vue/composition-api': optional: true dependencies: - '@vue/composition-api': 1.7.2(vue@3.3.4) vue: 3.3.4 dev: false @@ -10508,17 +10479,6 @@ packages: typescript: 5.2.2 dev: true - /vue@2.6.14: - resolution: {integrity: sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==} - dev: true - - /vue@2.7.14: - resolution: {integrity: sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==} - dependencies: - '@vue/compiler-sfc': 2.7.14 - csstype: 3.1.2 - dev: true - /vue@3.3.4: resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} dependencies: