Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/varden/src/components/VardenField.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
...args: Parameters<typeof useFormLib<T>>
function useForm<T, O>(
...args: Parameters<typeof useFormLib<T, O>>
): FormContext<T> & { __meta: Map<string, FieldMeta> } {
return useFormLib<T>(...args) as FormContext<T> & { __meta: Map<string, FieldMeta> };
return useFormLib<T, O>(...args) as FormContext<T> & { __meta: Map<string, FieldMeta> };
}

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: () => {},
});
Expand Down Expand Up @@ -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: () => {},
});
Expand Down
14 changes: 7 additions & 7 deletions packages/varden/src/components/VardenForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import VardenForm from './VardenForm.vue';
import VardenField from './VardenField.vue';
import type { FieldMeta } from '../field-metadata';

function useForm<T>(
...args: Parameters<typeof useFormLib<T>>
function useForm<T, O>(
...args: Parameters<typeof useFormLib<T, O>>
): FormContext<T> & { __meta: Map<string, FieldMeta> } {
return useFormLib<T>(...args) as FormContext<T> & { __meta: Map<string, FieldMeta> };
return useFormLib<T, O>(...args) as FormContext<T> & { __meta: Map<string, FieldMeta> };
}

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,
});

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
});
Expand Down
43 changes: 39 additions & 4 deletions packages/varden/src/lib.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,52 @@
import { describe, expect, it } from 'vitest';
import {
describe, expect, it, vi,
} from 'vitest';
import * as v from 'valibot';
import { effectScope } from 'vue';

import { useForm as useFormLib, type FormContext } from './lib';
import { useFieldValue } from './composables';
import type { FieldMeta } from './field-metadata';

function useForm<T>(
...args: Parameters<typeof useFormLib<T>>
function useForm<T, O>(
...args: Parameters<typeof useFormLib<T, O>>
): FormContext<T> & { __meta: Map<string, FieldMeta> } {
return useFormLib<T>(...args) as FormContext<T> & { __meta: Map<string, FieldMeta> };
return useFormLib<T, O>(...args) as FormContext<T> & { __meta: Map<string, FieldMeta> };
}

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({
Expand Down
36 changes: 24 additions & 12 deletions packages/varden/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import { createFieldMeta, type FieldMeta } from './field-metadata';

type PartialDeep<T> = T extends object ? { [K in keyof T]?: PartialDeep<T[K]> } : Partial<T>;

interface FormProps<T> {
schema: StandardSchemaV1<T>;
interface FormProps<T, O> {
schema: StandardSchemaV1<T, O>;
initial?: PartialDeep<T>;
onSubmit: (value: T) => Promise<void> | void;
onSubmit: (value: O) => Promise<void> | void;
cloneFn?: <A>(arg: A) => A;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
equalsFn?: (a: any, b: any) => boolean;
Expand Down Expand Up @@ -69,11 +69,11 @@ function strictEqualsFn(a: any, b: any): boolean {
}

// eslint-disable-next-line no-underscore-dangle
let _equalsFn: FormProps<unknown>['equalsFn'] & {} = strictEqualsFn;
let _equalsFn: FormProps<unknown, unknown>['equalsFn'] & {} = strictEqualsFn;
// eslint-disable-next-line no-underscore-dangle
let _cloneFn: FormProps<unknown>['cloneFn'] & {} = structuredClone;
let _cloneFn: FormProps<unknown, unknown>['cloneFn'] & {} = structuredClone;

export function defineVardenConfig(config: { equalsFn?: FormProps<unknown>['equalsFn']; cloneFn?: FormProps<unknown>['cloneFn'] }) {
export function defineVardenConfig(config: { equalsFn?: FormProps<unknown, unknown>['equalsFn']; cloneFn?: FormProps<unknown, unknown>['cloneFn'] }) {
if (config.equalsFn) _equalsFn = config.equalsFn;
if (config.cloneFn) _cloneFn = config.cloneFn;
}
Expand All @@ -83,13 +83,14 @@ export function resetVardenConfig() {
_cloneFn = structuredClone;
}

export function useForm<T = object>(props: FormProps<T>): FormContext<T> {
export function useForm<T, O>(props: FormProps<T, O>): FormContext<T> {
const {
initial, schema, onSubmit, cloneFn = _cloneFn, equalsFn = _equalsFn,
} = props;

const initialValues: PartialDeep<T> = cloneFn(initial ?? {} as PartialDeep<T>);

let outputValues: O | undefined;
const currentValues = ref<PartialDeep<T>>(cloneFn(initialValues));
const fields = reactive(new Map<string, FieldMeta>());
const valid = ref(true);
Expand Down Expand Up @@ -151,8 +152,13 @@ export function useForm<T = object>(props: FormProps<T>): FormContext<T> {
}
};

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<O>;

if ('value' in result) {
outputValues = result.value;
}

const issues = [...(result.issues ?? [])];
const paths = issues.map(getIssuePath);
Expand Down Expand Up @@ -252,8 +258,7 @@ export function useForm<T = object>(props: FormProps<T>): FormContext<T> {
return;
}

// TODO: properly coerce/transform validation result
onSubmit(cloneFn(currentValues.value));
onSubmit(outputValues!);
},
isDirty<Path extends Paths<T>>(path: Path): boolean {
// TODO: consider shipping dequal for deep equality
Expand All @@ -266,10 +271,17 @@ export function useForm<T = object>(props: FormProps<T>): FormContext<T> {
);
}

// 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);
Expand Down
Loading