From 1151cc574849564b33dae0ccbdfe3a613d9763bc Mon Sep 17 00:00:00 2001 From: Solant Date: Mon, 1 Jun 2026 21:58:08 +0200 Subject: [PATCH 1/3] fix!: allow multiple errors per field --- .../varden/src/components/VardenError.vue | 6 +-- .../varden/src/components/VardenField.spec.ts | 4 +- .../varden/src/components/VardenField.vue | 4 +- packages/varden/src/composables.ts | 14 +++---- packages/varden/src/field-metadata.ts | 4 +- packages/varden/src/index.ts | 2 +- packages/varden/src/lib.ts | 41 +++++++++++-------- packages/varden/src/standard-schema.ts | 20 ++++++++- 8 files changed, 59 insertions(+), 36 deletions(-) diff --git a/packages/varden/src/components/VardenError.vue b/packages/varden/src/components/VardenError.vue index 33377b9..3bb3a47 100644 --- a/packages/varden/src/components/VardenError.vue +++ b/packages/varden/src/components/VardenError.vue @@ -1,16 +1,16 @@ diff --git a/packages/varden/src/components/VardenField.spec.ts b/packages/varden/src/components/VardenField.spec.ts index e050296..0eb6e90 100644 --- a/packages/varden/src/components/VardenField.spec.ts +++ b/packages/varden/src/components/VardenField.spec.ts @@ -24,7 +24,7 @@ describe('meta management', () => { // @ts-expect-error typed vue component props: { form, path: 'name' }, slots: { - default: ({ field, error }: { field: { modelValue: string }; error: string | null }) => h('div', [ + default: ({ field, errors }: { field: { modelValue: string }; errors: readonly string[] | null }) => h('div', [ h('input', { 'data-testid': 'name-input', value: field.modelValue, @@ -33,7 +33,7 @@ describe('meta management', () => { field.modelValue = (e.target as HTMLInputElement).value; }, }), - h('span', { 'data-testid': 'error' }, error ?? ''), + h('span', { 'data-testid': 'error' }, errors?.[0] ?? ''), ]), }, }); diff --git a/packages/varden/src/components/VardenField.vue b/packages/varden/src/components/VardenField.vue index bfe0d1e..78070a5 100644 --- a/packages/varden/src/components/VardenField.vue +++ b/packages/varden/src/components/VardenField.vue @@ -11,7 +11,7 @@ const props = defineProps<{ const [ modelValue, onBlur, - error, + errors, ] = useField(props.form, props.path); function update(value: Get) { @@ -22,6 +22,6 @@ function update(value: Get) { diff --git a/packages/varden/src/composables.ts b/packages/varden/src/composables.ts index 1c5af46..2f15e32 100644 --- a/packages/varden/src/composables.ts +++ b/packages/varden/src/composables.ts @@ -32,7 +32,7 @@ function acquireField>(form: FormContext, path: Path return; } - (form as _FormContext).__meta.set(path, createFieldMeta(false, false, '', 1)); + (form as _FormContext).__meta.set(path, createFieldMeta(false, false, null, 1)); } export function useFieldDirty>( @@ -75,11 +75,11 @@ export function useFieldValue( }); } -export function useFieldError>( +export function useFieldErrors>( form: FormContext, path: MaybeRefOrGetter, -): ComputedRef { - return computed(() => form.getError(toValue(path))); +): ComputedRef | null> { + return computed(() => form.getErrors(toValue(path))); } export function useField( @@ -88,15 +88,15 @@ export function useField( ): [ modelValue: WritableComputedRef>>, touch: () => void, - error: ComputedRef, + errors: ComputedRef | null>, ] { const modelValue = useFieldValue(form, path); const touch = () => form.setTouched(toValue(path), true); - const error = useFieldError(form, path); + const errors = useFieldErrors(form, path); return [ modelValue, touch, - error, + errors, ] as const; } diff --git a/packages/varden/src/field-metadata.ts b/packages/varden/src/field-metadata.ts index 7d8108c..3770726 100644 --- a/packages/varden/src/field-metadata.ts +++ b/packages/varden/src/field-metadata.ts @@ -1,14 +1,14 @@ export interface FieldMeta { touched: boolean; dirty: boolean; - error: string | null; + error: string[] | null; refCount: number; } export function createFieldMeta( touched: boolean, dirty: boolean, - error: string | null, + error: string[] | null, refCount: number, ): FieldMeta { return { diff --git a/packages/varden/src/index.ts b/packages/varden/src/index.ts index 9b20abc..d3b5405 100644 --- a/packages/varden/src/index.ts +++ b/packages/varden/src/index.ts @@ -3,7 +3,7 @@ export { } from './lib'; export { - useFieldValue, useFieldDirty, useFieldError, useFieldTouched, useField, + useFieldValue, useFieldDirty, useFieldErrors, useFieldTouched, useField, } from './composables'; export { default as VardenForm } from './components/VardenForm.vue'; diff --git a/packages/varden/src/lib.ts b/packages/varden/src/lib.ts index 081b401..08fa7de 100644 --- a/packages/varden/src/lib.ts +++ b/packages/varden/src/lib.ts @@ -8,7 +8,7 @@ import { type DeepReadonly, } from 'vue'; -import { getIssuePath, type StandardSchemaV1 } from './standard-schema'; +import { getIssues, type StandardSchemaV1 } from './standard-schema'; import { type Paths, type Get, get, set, del, toCompiledPath, Empty, @@ -46,7 +46,7 @@ export interface FormContext { isTouched>(path: Path): boolean; valid: Ref; isDirty>(path: Path): boolean; - getError>(path: Path): string | null; + getErrors>(path: Path): string[] | null; submit(): void; pop>(path: Path): undefined | GetArray; shift>(path: Path): undefined | GetArray; @@ -161,34 +161,41 @@ export function useForm(props: FormProps): FormContext { outputValues = result.value; } - const issues = [...(result.issues ?? [])]; - const paths = issues.map(getIssuePath); + const issues = getIssues(result.issues); valid.value = issues.length === 0; for (const [field, meta] of fields) { - const index = paths.indexOf(field); - if (index === -1) { - meta.error = ''; + const fieldIssues = issues.filter((val) => val.path === field); + if (fieldIssues.length === 0) { + meta.error = null; // eslint-disable-next-line no-continue continue; + } else { + meta.error = fieldIssues.map((val) => val.message); } - meta.error = issues[index]!.message; - - issues.splice(index, 1); - paths.splice(index, 1); + for (let i = 0; i < fieldIssues.length; i += 1) { + issues.splice(issues.indexOf(fieldIssues[i]!), 1); + } } if (!issues.length) { return; } // proceed with unregistered paths - for (let index = 0; index < paths.length; index += 1) { - const path = paths[index]!; - const error = issues[index]!.message; + for (let index = 0; index < issues.length; index += 1) { + const { path, message } = issues[index]!; + if (!path) { + // eslint-disable-next-line no-continue + continue; + } - fields.set(path, createFieldMeta(false, false, error, 0)); + if (fields.has(path)) { + fields.get(path)!.error!.push(message); + } else { + fields.set(path, createFieldMeta(false, false, [message], 0)); + } } } @@ -212,7 +219,7 @@ export function useForm(props: FormProps): FormContext { if (meta) { meta.dirty = isDirty; } else { - fields.set(stringPath, createFieldMeta(false, isDirty, '', 0)); + fields.set(stringPath, createFieldMeta(false, isDirty, null, 0)); } // cleanup child fields @@ -297,7 +304,7 @@ export function useForm(props: FormProps): FormContext { isTouched>(path: Path): boolean { return fields.get(path)?.touched ?? false; }, - getError>(path: Path): string | null { + getErrors>(path: Path): string[] | null { return fields.get(path)?.error ?? null; }, // arrays diff --git a/packages/varden/src/standard-schema.ts b/packages/varden/src/standard-schema.ts index e051375..8c0163e 100644 --- a/packages/varden/src/standard-schema.ts +++ b/packages/varden/src/standard-schema.ts @@ -68,9 +68,25 @@ export declare namespace StandardSchemaV1 { export type InferOutput = NonNullable['output']; } -export function getIssuePath(issue: StandardSchemaV1.Issue) { +interface GenericIssue { + path?: string; + message: string; +} + +export function getIssues(issues?: readonly StandardSchemaV1.Issue[]): GenericIssue[] { + if (issues === undefined) { + return []; + } + + return issues.map((issue) => ({ + path: getIssuePath(issue), + message: issue.message, + })); +} + +function getIssuePath(issue: StandardSchemaV1.Issue) { if (!issue.path) { - return ''; + return issue.path; } const propertyKeys = issue.path.map((p) => { From a1ffbf205be44a8cceafbb439c0fc6e1c9494f48 Mon Sep 17 00:00:00 2001 From: Solant Date: Mon, 1 Jun 2026 22:00:36 +0200 Subject: [PATCH 2/3] docs: update readme --- packages/varden/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/varden/README.md b/packages/varden/README.md index b055814..412608e 100644 --- a/packages/varden/README.md +++ b/packages/varden/README.md @@ -40,16 +40,16 @@ Remember to pass `form` prop to both form and field components and apply `field`