diff --git a/packages/varden/src/components/VardenField.spec.ts b/packages/varden/src/components/VardenField.spec.ts index f7dc8c2..e050296 100644 --- a/packages/varden/src/components/VardenField.spec.ts +++ b/packages/varden/src/components/VardenField.spec.ts @@ -7,15 +7,15 @@ import { useForm as useFormLib, type FormContext } from '../lib'; import type { FieldMeta } from '../field-metadata'; import VardenField from './VardenField.vue'; -function useForm( - ...args: Parameters> +function useForm( + ...args: Parameters> ): FormContext & { __meta: Map } { - return useFormLib(...args) as FormContext & { __meta: Map }; + return useFormLib(...args) as FormContext & { __meta: Map }; } describe('meta management', () => { it('should render field value through component', async () => { - const form = useForm<{ name: string }>({ + const form = useForm({ schema: v.object({ name: v.string() }), onSubmit: () => {}, }); @@ -47,7 +47,7 @@ describe('meta management', () => { }); it('should not reset field value when another component instance still references it', async () => { - const form = useForm<{ name: string }>({ + const form = useForm({ schema: v.object({ name: v.string() }), onSubmit: () => {}, }); diff --git a/packages/varden/src/components/VardenForm.spec.ts b/packages/varden/src/components/VardenForm.spec.ts index 99af9c0..7269d1c 100644 --- a/packages/varden/src/components/VardenForm.spec.ts +++ b/packages/varden/src/components/VardenForm.spec.ts @@ -10,17 +10,17 @@ import VardenForm from './VardenForm.vue'; import VardenField from './VardenField.vue'; import type { FieldMeta } from '../field-metadata'; -function useForm( - ...args: Parameters> +function useForm( + ...args: Parameters> ): FormContext & { __meta: Map } { - return useFormLib(...args) as FormContext & { __meta: Map }; + return useFormLib(...args) as FormContext & { __meta: Map }; } describe('meta management', () => { it('should preserve values when fields are actively referenced', async () => { const onSubmit = vi.fn(); - const form = useForm<{ name: string; email: string }>({ - schema: v.object({ name: v.string(), email: v.string() }), + const form = useForm({ + schema: v.object({ name: v.optional(v.string()), email: v.optional(v.string()) }), onSubmit, }); @@ -52,7 +52,7 @@ describe('meta management', () => { it('should reset form values when reset is called', async () => { const onSubmit = vi.fn(); - const form = useForm<{ name: string }>({ + const form = useForm({ schema: v.object({ name: v.string() }), initial: { name: 'Initial' }, onSubmit, @@ -81,7 +81,7 @@ describe('meta management', () => { it('should not reset field data preemptively while component still references it', async () => { const onSubmit = vi.fn(); - const form = useForm<{ name: string }>({ + const form = useForm({ schema: v.object({ name: v.string() }), onSubmit, }); diff --git a/packages/varden/src/lib.spec.ts b/packages/varden/src/lib.spec.ts index 76bcd79..58249e5 100644 --- a/packages/varden/src/lib.spec.ts +++ b/packages/varden/src/lib.spec.ts @@ -1,4 +1,6 @@ -import { describe, expect, it } from 'vitest'; +import { + describe, expect, it, vi, +} from 'vitest'; import * as v from 'valibot'; import { effectScope } from 'vue'; @@ -6,12 +8,45 @@ import { useForm as useFormLib, type FormContext } from './lib'; import { useFieldValue } from './composables'; import type { FieldMeta } from './field-metadata'; -function useForm( - ...args: Parameters> +function useForm( + ...args: Parameters> ): FormContext & { __meta: Map } { - return useFormLib(...args) as FormContext & { __meta: Map }; + return useFormLib(...args) as FormContext & { __meta: Map }; } +describe('form submit', () => { + it('should submit form data without cloning issue', async () => { + const onSubmit = vi.fn(); + + const form = useForm({ + schema: v.object({ name: v.string() }), + onSubmit, + }); + + form.setValue('name', 'Test'); + form.submit(); + expect(onSubmit).toHaveBeenCalledWith({ name: 'Test' }); + }); + + it('should transform values according to schema', () => { + const onSubmit = vi.fn(); + + const form = useForm({ + schema: v.object({ + name: v.pipe( + v.string(), + v.transform((input) => input.length), + ), + }), + onSubmit, + }); + + form.setValue('name', 'Test'); + form.submit(); + expect(onSubmit).toHaveBeenCalledWith({ name: 4 }); + }); +}); + describe('meta management', () => { it('should not reset field that is currently referenced', () => { const form = useForm({ diff --git a/packages/varden/src/lib.ts b/packages/varden/src/lib.ts index 35f4370..cc9279a 100644 --- a/packages/varden/src/lib.ts +++ b/packages/varden/src/lib.ts @@ -22,10 +22,10 @@ import { createFieldMeta, type FieldMeta } from './field-metadata'; type PartialDeep = T extends object ? { [K in keyof T]?: PartialDeep } : Partial; -interface FormProps { - schema: StandardSchemaV1; +interface FormProps { + schema: StandardSchemaV1; initial?: PartialDeep; - onSubmit: (value: T) => Promise | void; + onSubmit: (value: O) => Promise | void; cloneFn?: (arg: A) => A; // eslint-disable-next-line @typescript-eslint/no-explicit-any equalsFn?: (a: any, b: any) => boolean; @@ -69,11 +69,11 @@ function strictEqualsFn(a: any, b: any): boolean { } // eslint-disable-next-line no-underscore-dangle -let _equalsFn: FormProps['equalsFn'] & {} = strictEqualsFn; +let _equalsFn: FormProps['equalsFn'] & {} = strictEqualsFn; // eslint-disable-next-line no-underscore-dangle -let _cloneFn: FormProps['cloneFn'] & {} = structuredClone; +let _cloneFn: FormProps['cloneFn'] & {} = structuredClone; -export function defineVardenConfig(config: { equalsFn?: FormProps['equalsFn']; cloneFn?: FormProps['cloneFn'] }) { +export function defineVardenConfig(config: { equalsFn?: FormProps['equalsFn']; cloneFn?: FormProps['cloneFn'] }) { if (config.equalsFn) _equalsFn = config.equalsFn; if (config.cloneFn) _cloneFn = config.cloneFn; } @@ -83,13 +83,14 @@ export function resetVardenConfig() { _cloneFn = structuredClone; } -export function useForm(props: FormProps): FormContext { +export function useForm(props: FormProps): FormContext { const { initial, schema, onSubmit, cloneFn = _cloneFn, equalsFn = _equalsFn, } = props; const initialValues: PartialDeep = cloneFn(initial ?? {} as PartialDeep); + let outputValues: O | undefined; const currentValues = ref>(cloneFn(initialValues)); const fields = reactive(new Map()); const valid = ref(true); @@ -151,8 +152,13 @@ export function useForm(props: FormProps): FormContext { } }; - async function applyValidation() { - const result = await schema['~standard'].validate(currentValues.value); + function applyValidation() { + // TODO: allow async validation + const result = schema['~standard'].validate(currentValues.value) as StandardSchemaV1.Result; + + if ('value' in result) { + outputValues = result.value; + } const issues = [...(result.issues ?? [])]; const paths = issues.map(getIssuePath); @@ -252,8 +258,7 @@ export function useForm(props: FormProps): FormContext { return; } - // TODO: properly coerce/transform validation result - onSubmit(cloneFn(currentValues.value)); + onSubmit(outputValues!); }, isDirty>(path: Path): boolean { // TODO: consider shipping dequal for deep equality @@ -266,10 +271,17 @@ export function useForm(props: FormProps): FormContext { ); } + // TODO: refactor to track dirty state + let self: FieldMeta | null = null; for (const [field, meta] of fields) { - if (field === path) return meta.dirty; + if (field === path) { + self = meta; + // eslint-disable-next-line no-continue + continue; + } if (field.startsWith(`${path}.`) && meta.dirty === true) return true; } + if (self !== null) return self.dirty; // TODO: should be tracked after first check? const compiledPath = toCompiledPath(path);