Skip to content

Commit 2ccc638

Browse files
authored
fix: expose issue.path in .allIssues() for field containers (#14784)
fixes #14688 Changes .allIssues() from { message: string } to the following to indicate where in the field container the issue occurred.
1 parent cd72d94 commit 2ccc638

File tree

8 files changed

+58
-14
lines changed

8 files changed

+58
-14
lines changed

.changeset/thin-frogs-create.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: expose `issue.path` in `.allIssues()`

packages/kit/src/exports/public.d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,12 +1911,12 @@ export type RemoteFormField<Value extends RemoteFormFieldValue> = RemoteFormFiel
19111911

19121912
type RemoteFormFieldContainer<Value> = RemoteFormFieldMethods<Value> & {
19131913
/** Validation issues belonging to this or any of the fields that belong to it, if any */
1914-
allIssues(): RemoteFormIssue[] | undefined;
1914+
allIssues(): RemoteFormAllIssue[] | undefined;
19151915
};
19161916

19171917
type UnknownField<Value> = RemoteFormFieldMethods<Value> & {
19181918
/** Validation issues belonging to this or any of the fields that belong to it, if any */
1919-
allIssues(): RemoteFormIssue[] | undefined;
1919+
allIssues(): RemoteFormAllIssue[] | undefined;
19201920
/**
19211921
* Returns an object that can be spread onto an input element with the correct type attribute,
19221922
* aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
@@ -1965,6 +1965,10 @@ export interface RemoteFormIssue {
19651965
message: string;
19661966
}
19671967

1968+
export interface RemoteFormAllIssue extends RemoteFormIssue {
1969+
path: Array<string | number>;
1970+
}
1971+
19681972
// If the schema specifies `id` as a string or number, ensure that `for(...)`
19691973
// only accepts that type. Otherwise, accept `string | number`
19701974
type ExtractId<Input> = Input extends { id: infer Id }

packages/kit/src/runtime/app/server/remote/form.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export function form(validate_or_fn, maybe_fn) {
153153
const validated = await schema?.['~standard'].validate(data);
154154

155155
if (validate_only) {
156-
return validated?.issues ?? [];
156+
return validated?.issues?.map((issue) => normalize_issue(issue, true)) ?? [];
157157
}
158158

159159
if (validated?.issues !== undefined) {

packages/kit/src/runtime/form-utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
247247

248248
if (prop === 'allIssues') {
249249
return all_issues?.map((issue) => ({
250+
path: issue.path,
250251
message: issue.message
251252
}));
252253
}

packages/kit/test/apps/basics/src/routes/remote/form/validate/+page.svelte

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script>
2-
import { my_form } from './form.remote.js';
2+
import { issue_path_form, my_form } from './form.remote.js';
33
import * as v from 'valibot';
44
55
const schema = v.object({
@@ -11,7 +11,7 @@
1111
$inspect(my_form.fields.allIssues());
1212
</script>
1313

14-
<form {...my_form.preflight(schema)} oninput={() => my_form.validate()}>
14+
<form id="my-form" {...my_form.preflight(schema)} oninput={() => my_form.validate()}>
1515
{#each my_form.fields.foo.issues() as issue}
1616
<p>{issue.message}</p>
1717
{/each}
@@ -39,3 +39,15 @@
3939
>
4040
trigger validation
4141
</button>
42+
43+
<form id="issue-path-form" {...issue_path_form}>
44+
<input {...issue_path_form.fields.nested.value.as('text')} />
45+
<button
46+
type="button"
47+
id="validate"
48+
onclick={() => issue_path_form.validate({ includeUntouched: true })}
49+
>
50+
Validate
51+
</button>
52+
<pre id="allIssues">{JSON.stringify(issue_path_form.fields.allIssues())}</pre>
53+
</form>

packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,14 @@ export const my_form = form(
1616
console.log(data);
1717
}
1818
);
19+
20+
export const issue_path_form = form(
21+
v.object({
22+
nested: v.object({
23+
value: v.pipe(v.string(), v.minLength(3))
24+
})
25+
}),
26+
async (data) => {
27+
return data;
28+
}
29+
);

packages/kit/test/apps/basics/test/test.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,31 +1871,38 @@ test.describe('remote functions', () => {
18711871

18721872
await page.goto('/remote/form/validate');
18731873

1874+
const myForm = page.locator('form#my-form');
18741875
const foo = page.locator('input[name="foo"]');
18751876
const bar = page.locator('input[name="bar"]');
18761877
const submit = page.locator('button:has-text("imperative validation")');
18771878

18781879
await foo.fill('a');
1879-
await expect(page.locator('form')).not.toContainText('Invalid type: Expected');
1880+
await expect(myForm).not.toContainText('Invalid type: Expected');
18801881

18811882
await bar.fill('g');
1882-
await expect(page.locator('form')).toContainText(
1883-
'Invalid type: Expected ("d" | "e") but received "g"'
1884-
);
1883+
await expect(myForm).toContainText('Invalid type: Expected ("d" | "e") but received "g"');
18851884

18861885
await bar.fill('d');
1887-
await expect(page.locator('form')).not.toContainText('Invalid type: Expected');
1886+
await expect(myForm).not.toContainText('Invalid type: Expected');
18881887

18891888
await page.locator('#trigger-validate').click();
1890-
await expect(page.locator('form')).toContainText(
1889+
await expect(myForm).toContainText(
18911890
'Invalid type: Expected "submitter" but received "incorrect_value"'
18921891
);
18931892

18941893
// Test imperative validation
18951894
await foo.fill('c');
18961895
await bar.fill('d');
18971896
await submit.click();
1898-
await expect(page.locator('form')).toContainText('Imperative: foo cannot be c');
1897+
await expect(myForm).toContainText('Imperative: foo cannot be c');
1898+
1899+
const nestedValue = page.locator('input[name="nested.value"]');
1900+
const validate = page.locator('button#validate');
1901+
const allIssues = page.locator('#allIssues');
1902+
1903+
await nestedValue.fill('in');
1904+
await validate.click();
1905+
await expect(allIssues).toContainText('"path":["nested","value"]');
18991906
});
19001907

19011908
test('form inputs excludes underscore-prefixed fields', async ({ page, javaScriptEnabled }) => {

packages/kit/types/index.d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,12 +1887,12 @@ declare module '@sveltejs/kit' {
18871887

18881888
type RemoteFormFieldContainer<Value> = RemoteFormFieldMethods<Value> & {
18891889
/** Validation issues belonging to this or any of the fields that belong to it, if any */
1890-
allIssues(): RemoteFormIssue[] | undefined;
1890+
allIssues(): RemoteFormAllIssue[] | undefined;
18911891
};
18921892

18931893
type UnknownField<Value> = RemoteFormFieldMethods<Value> & {
18941894
/** Validation issues belonging to this or any of the fields that belong to it, if any */
1895-
allIssues(): RemoteFormIssue[] | undefined;
1895+
allIssues(): RemoteFormAllIssue[] | undefined;
18961896
/**
18971897
* Returns an object that can be spread onto an input element with the correct type attribute,
18981898
* aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
@@ -1941,6 +1941,10 @@ declare module '@sveltejs/kit' {
19411941
message: string;
19421942
}
19431943

1944+
export interface RemoteFormAllIssue extends RemoteFormIssue {
1945+
path: Array<string | number>;
1946+
}
1947+
19441948
// If the schema specifies `id` as a string or number, ensure that `for(...)`
19451949
// only accepts that type. Otherwise, accept `string | number`
19461950
type ExtractId<Input> = Input extends { id: infer Id }

0 commit comments

Comments
 (0)