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
6 changes: 3 additions & 3 deletions packages/varden/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@
},
"devDependencies": {
"@types/node": "^24.12.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue": "^6.0.7",
"@vitest/browser": "3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"@vue/test-utils": "^2.4.6",
"@vue/test-utils": "^2.4.10",
"@vue/tsconfig": "^0.8.1",
"changelogen": "^0.6.2",
"dequal": "^2.0.3",
"eslint-plugin-package-json": "^1.1.0",
"playwright": "^1.58.2",
"playwright": "^1.60.0",
"tsdown": "^0.21.6",
"typescript": "~6.0.2",
"unplugin-vue": "^7.1.1",
Expand Down
129 changes: 93 additions & 36 deletions packages/varden/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
Empty,
isEmptyObject,
type CompiledPath,
type ArrayPaths,
type GetArray,
} from './path';
import { createFieldMeta, type FieldMeta } from './field-metadata';

Expand Down Expand Up @@ -45,6 +47,16 @@ export interface FormContext<T> {
isDirty<Path extends Paths<T>>(path: Path): boolean;
getError<Path extends Paths<T>>(path: Path): string | null;
submit(): void;
pop<Path extends ArrayPaths<T>>(path: Path): undefined | GetArray<T, Path>;
shift<Path extends ArrayPaths<T>>(path: Path): undefined | GetArray<T, Path>;
push<Path extends ArrayPaths<T>>(path: Path, value: GetArray<T, Path>): void;
unshift<Path extends ArrayPaths<T>>(path: Path, value: GetArray<T, Path>): void;
splice<Path extends ArrayPaths<T>>(
path: Path,
index?: number,
deleteCount?: number,
...items: GetArray<T, Path>[]
): void;
}

export interface _FormContext<T> extends FormContext<T> {
Expand Down Expand Up @@ -182,49 +194,48 @@ export function useForm<T = object>(props: FormProps<T>): FormContext<T> {
return false;
});

const setValue: FormContext<T>['setValue'] = (path, value) => {
const compiledPath = Array.isArray(path) ? path : toCompiledPath(path);
const stringPath = typeof path === 'string' ? path : compiledPath.join('.');

set(currentValues.value, compiledPath, cloneFn(value));

const meta = fields.get(stringPath);
const isDirty = !equalsFn(get(initialValues, compiledPath, Empty), value);
if (meta) {
meta.dirty = isDirty;
} else {
fields.set(stringPath, createFieldMeta(false, isDirty, '', 0));
}

// cleanup child fields
for (const [nestedPath, nestedMeta] of fields) {
if (nestedPath.startsWith(`${stringPath}.`)) {
if (nestedMeta !== undefined) {
if (nestedMeta.refCount === 0) {
fields.delete(nestedPath);
} else {
const compiledNestedPath = toCompiledPath(nestedPath);
nestedMeta.dirty = !equalsFn(
get(initialValues, compiledNestedPath),
get(currentValues.value, compiledNestedPath),
);
}
}
}
}

applyValidation();
};

return {
values: readonly(currentValues) as FormContext<T>['values'],
dirty,
// @ts-expect-error private field
__meta: fields,
reset,
resetField,
setValue<Path extends Paths<T>, Value extends Get<T, Path>>(
path: Path | CompiledPath,
value: Value | undefined,
) {
const compiledPath = Array.isArray(path) ? path : toCompiledPath(path);
const stringPath = typeof path === 'string' ? path : compiledPath.join('.');

set(currentValues.value, compiledPath, cloneFn(value));

const meta = fields.get(stringPath);
const isDirty = !equalsFn(get(initialValues, compiledPath, Empty), value);
if (meta) {
meta.dirty = isDirty;
} else {
fields.set(stringPath, createFieldMeta(false, isDirty, '', 0));
}

// cleanup child fields
for (const [nestedPath, nestedMeta] of fields) {
if (nestedPath.startsWith(`${stringPath}.`)) {
if (nestedMeta !== undefined) {
if (nestedMeta.refCount === 0) {
fields.delete(nestedPath);
} else {
const compiledNestedPath = toCompiledPath(nestedPath);
nestedMeta.dirty = !equalsFn(
get(initialValues, compiledNestedPath),
get(currentValues.value, compiledNestedPath),
);
}
}
}
}

applyValidation();
},
setValue,
getValue<Path extends Paths<T>, Value extends Get<T, Path>>(path: Path | CompiledPath): Value {
const a = get(currentValues.value, Array.isArray(path) ? path : toCompiledPath(path));
return cloneFn(toRaw(a));
Expand Down Expand Up @@ -273,5 +284,51 @@ export function useForm<T = object>(props: FormProps<T>): FormContext<T> {
getError<Path extends Paths<T>>(path: Path): string | null {
return fields.get(path)?.error ?? null;
},
// arrays
pop<Path extends ArrayPaths<T>>(path: Path): undefined | GetArray<T, Path> {
const compiledPath = toCompiledPath(path);
const value = get(currentValues.value, compiledPath);
return Array.isArray(value) ? value.pop() : undefined;
},
shift<Path extends ArrayPaths<T>>(path: Path): undefined | GetArray<T, Path> {
const compiledPath = toCompiledPath(path);
const value = get(currentValues.value, compiledPath);
return Array.isArray(value) ? value.shift() : undefined;
},
push<Path extends ArrayPaths<T>>(path: Path, value: GetArray<T, Path>): void {
const compiledPath = toCompiledPath(path);
const arr = get(currentValues.value, compiledPath);
if (Array.isArray(arr)) {
arr.push(cloneFn(value));
} else {
// @ts-expect-error GetArray/Get conversion
setValue(path, [cloneFn(value)]);
}
},
unshift<Path extends ArrayPaths<T>>(path: Path, value: GetArray<T, Path>): void {
const compiledPath = toCompiledPath(path);
const arr = get(currentValues.value, compiledPath);
if (Array.isArray(arr)) {
arr.unshift(cloneFn(value));
} else {
// @ts-expect-error GetArray/Get conversion
setValue(path, [cloneFn(value)]);
}
},
splice<Path extends ArrayPaths<T>>(
path: Path,
index?: number,
deleteCount?: number,
...items: GetArray<T, Path>[]
): void {
const compiledPath = toCompiledPath(path);
const arr = get(currentValues.value, compiledPath);
if (Array.isArray(arr)) {
arr.splice(index ?? 0, deleteCount ?? 0, ...(items ?? []).map((v) => cloneFn(v)));
} else if (Array.isArray(items)) {
// @ts-expect-error GetArray/Get conversion
setValue(path, items.map((v) => cloneFn(v)));
}
},
};
}
41 changes: 40 additions & 1 deletion packages/varden/src/path.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,46 @@
export type Paths<T> = T extends Array<infer U>
? `${Paths<U>}`
? (U extends object ? never : `${number}`) | `${number}.${Paths<U>}`
: T extends object
? {
[K in keyof T & (string | number)]: K extends string ? `${K}` | `${K}.${Paths<T[K]>}` : never;
}[keyof T & (string | number)]
: never;

export type ArrayPaths<T> = T extends Array<infer U>
? (U extends Array<unknown> ? `${number}` : never) | `${number}.${ArrayPaths<U>}`
: T extends object
? {
[K in keyof T & (string | number)]: K extends string
? T[K] extends Array<unknown>
? `${K}` | `${K}.${ArrayPaths<T[K]>}`
: `${K}.${ArrayPaths<T[K]>}`
: never;
}[keyof T & (string | number)]
: never;

export type Get<T, P extends Paths<T>> = P extends `${infer K}.${infer R}`
? K extends keyof T
? R extends Paths<T[K]>
? Get<T[K], R>
: T[K] extends Array<infer U>
? R extends `${number}`
? U
: never
: never
: T extends Array<infer U>
? K extends `${number}`
? Get<U, R extends Paths<U> ? R : never>
: never
: never
: P extends keyof T
? T[P]
: T extends Array<infer U>
? P extends `${number}`
? U
: never
: never;

type _GetArray<T, P extends ArrayPaths<T>> = P extends `${infer K}.${infer R}`
? K extends keyof T
? R extends Paths<T[K]>
? Get<T[K], R>
Expand All @@ -16,6 +50,11 @@ export type Get<T, P extends Paths<T>> = P extends `${infer K}.${infer R}`
? T[P]
: never;

// Specialized Get for ArrayPaths as a workaround for tsc stack overflow error
export type GetArray<T, P extends ArrayPaths<T>> = _GetArray<T, P> extends Array<infer R>
? R
: never;

export type CompiledPath = Array<string | number>;

export function toCompiledPath(path: string): CompiledPath {
Expand Down
Loading
Loading