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
7 changes: 4 additions & 3 deletions documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,9 +472,9 @@ export const buy_hotcakes = form(
await buy(data.qty);
} catch (e) {
if (e.code === 'OUT_OF_STOCK') {
invalid([
invalid(
invalid.qty(`we don't have enough hotcakes`)
]);
);
}
}
}
Expand All @@ -483,7 +483,8 @@ export const buy_hotcakes = form(

The `invalid` function works as both a function and a proxy:

- Call `invalid([...])` with an array of issues to throw a validation error
- Call `invalid(issue1, issue2, ...issueN)` to throw a validation error
- If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`)
- Use `invalid.fieldName(message)` to create an issue for a specific field. Like `fields` this is type-safe and you can use regular property access syntax to create issues for deeply nested objects (e.g. `invalid.profile.email('Email already exists')` or `invalid.items[0].qty('Insufficient stock')`)

### Validation
Expand Down
19 changes: 11 additions & 8 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1959,23 +1959,26 @@ type InvalidField<T> =
/**
* A function and proxy object used to imperatively create validation errors in form handlers.
*
* Call `invalid([...])` with an array of issues to throw a validation error.
* Call `invalid(issue1, issue2, ...issueN)` to throw a validation error.
* If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`)
* Access properties to create field-specific issues: `invalid.fieldName('message')`.
* The type structure mirrors the input data structure for type-safe field access.
*
* @example
* ```ts
* invalid([
* invalid('Username or password is invalid');
* ```
*
* @example
* ```ts
* invalid(
* invalid.username('Username is taken'),
* invalid.items[0].qty('Insufficient stock')
* ]);
* );
* ```
*/
export type Invalid<Input = any> = ((issues: StandardSchemaV1.Issue[]) => never) &
InvalidField<Input> & {
/** Create an issue for the root of the form */
$: (message: string) => StandardSchemaV1.Issue;
};
export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) &
InvalidField<Input>;

/**
* The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
Expand Down
17 changes: 14 additions & 3 deletions packages/kit/src/runtime/app/server/remote/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,22 @@ function handle_issues(output, issues, is_remote_request, form_data) {
*/
function create_invalid() {
/**
* @param {StandardSchemaV1.Issue[]} issues
* @param {...(string | StandardSchemaV1.Issue)} issues
* @returns {never}
*/
function invalid(issues) {
throw new ValidationError(issues);
function invalid(...issues) {
throw new ValidationError(
issues.map((issue) => {
if (typeof issue === 'string') {
return {
path: [],
message: issue
};
}

return issue;
})
);
}

return /** @type {import('@sveltejs/kit').Invalid} */ (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const my_form = form(
async (data, invalid) => {
// Test imperative validation
if (data.foo === 'c') {
invalid([invalid.foo('Imperative: foo cannot be c')]);
invalid(invalid.foo('Imperative: foo cannot be c'));
}

console.log(data);
Expand Down
38 changes: 19 additions & 19 deletions packages/kit/test/types/remote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,12 @@ function form_tests() {
const q = query(() => '');
const f = form('unchecked', (data: { input: string }, invalid) => {
data.input;
invalid([
invalid.$('foo'),
invalid(
'foo',
invalid.input('bar'),
// @ts-expect-error
invalid.nonexistent.prop('baz')
]);
);
return { success: true };
});

Expand All @@ -193,12 +193,12 @@ function form_tests() {
data.nonexistent;
// @ts-expect-error
data.a === 123;
invalid([
invalid.$('foo'),
invalid(
'foo',
invalid.nested.prop('bar'),
// @ts-expect-error
invalid.nonexistent.prop('baz')
]);
);
return { success: true };
}
);
Expand Down Expand Up @@ -229,12 +229,12 @@ function form_tests() {
data.nonexistent;
// @ts-expect-error
data.a === 123;
invalid([
invalid.$('foo'),
invalid(
'foo',
invalid.nested.prop('bar'),
// @ts-expect-error
invalid.nonexistent.prop('baz')
]);
);
return { success: true };
}
);
Expand Down Expand Up @@ -268,12 +268,12 @@ function form_tests() {
data.bar === 'c';
// @ts-expect-error
data.foo === 'e';
invalid([
invalid.$('foo'),
invalid(
'foo',
invalid.bar('bar'),
// @ts-expect-error
invalid.nonexistent.prop('baz')
]);
);
return { success: true };
}
);
Expand All @@ -296,12 +296,12 @@ function form_tests() {
data.array[0].array[0] === 'a';
// @ts-expect-error
data.array[0].array[0] === 1;
invalid([
invalid.$('foo'),
invalid(
'foo',
invalid.array[0].prop('bar'),
// @ts-expect-error
invalid.nonexistent.prop('baz')
]);
);
return { success: true };
}
);
Expand All @@ -323,7 +323,7 @@ function form_tests() {
const f7 = form(null as any, (data, invalid) => {
data.a === '';
data.nested?.prop === '';
invalid([invalid.$('foo'), invalid.nested.prop('bar')]);
invalid('foo', invalid.nested.prop('bar'));
return { success: true };
});
// @ts-expect-error
Expand All @@ -335,11 +335,11 @@ function form_tests() {

// no schema
const f8 = form((invalid) => {
invalid([
invalid.$('foo'),
invalid(
'foo',
// @ts-expect-error
invalid.x('bar')
]);
);
});
// @ts-expect-error
f8.fields.x;
Expand Down
19 changes: 11 additions & 8 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1935,23 +1935,26 @@ declare module '@sveltejs/kit' {
/**
* A function and proxy object used to imperatively create validation errors in form handlers.
*
* Call `invalid([...])` with an array of issues to throw a validation error.
* Call `invalid(issue1, issue2, ...issueN)` to throw a validation error.
* If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`)
* Access properties to create field-specific issues: `invalid.fieldName('message')`.
* The type structure mirrors the input data structure for type-safe field access.
*
* @example
* ```ts
* invalid([
* invalid('Username or password is invalid');
* ```
*
* @example
* ```ts
* invalid(
* invalid.username('Username is taken'),
* invalid.items[0].qty('Insufficient stock')
* ]);
* );
* ```
*/
export type Invalid<Input = any> = ((issues: StandardSchemaV1.Issue[]) => never) &
InvalidField<Input> & {
/** Create an issue for the root of the form */
$: (message: string) => StandardSchemaV1.Issue;
};
export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) &
InvalidField<Input>;

/**
* The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
Expand Down