extends string | number | boolean | File
diff --git a/packages/kit/src/runtime/form-utils.svelte.js b/packages/kit/src/runtime/form-utils.svelte.js
index 4d0ff75d7318..7cbe614e6026 100644
--- a/packages/kit/src/runtime/form-utils.svelte.js
+++ b/packages/kit/src/runtime/form-utils.svelte.js
@@ -216,6 +216,82 @@ export function create_field_proxy(target, get_input, depend, set_input, get_iss
]);
}
+ if (prop === 'push') {
+ const push_func = function (/** @type {any} */ newValue) {
+ set_input(path, [...get_value(), newValue]);
+ };
+ return create_field_proxy(push_func, get_input, depend, set_input, get_issues, [
+ ...path,
+ prop
+ ]);
+ }
+
+ if (prop === 'unshift') {
+ const unshift_func = function (/** @type {any} */ newValue) {
+ set_input(path, [newValue, ...get_value()]);
+ };
+ return create_field_proxy(unshift_func, get_input, depend, set_input, get_issues, [
+ ...path,
+ prop
+ ]);
+ }
+
+ if (prop === 'pop') {
+ const pop_func = function () {
+ const value = get_value();
+ const output = value.pop();
+ set_input(path, value);
+ return output;
+ };
+ return create_field_proxy(pop_func, get_input, depend, set_input, get_issues, [
+ ...path,
+ prop
+ ]);
+ }
+
+ if (prop === 'shift') {
+ const shift_func = function () {
+ const value = get_value();
+ const output = value.shift();
+ set_input(path, value);
+ return output;
+ };
+ return create_field_proxy(shift_func, get_input, depend, set_input, get_issues, [
+ ...path,
+ prop
+ ]);
+ }
+
+ if (prop === 'splice') {
+ const splice_func = function (
+ /** @type {number} */ start,
+ /** @type {number} */ deleteCount,
+ /** @type {any[]} */ ...insert
+ ) {
+ const value = get_value();
+ const output = value.splice(start, deleteCount, ...insert);
+ set_input(path, value);
+ return output;
+ };
+ return create_field_proxy(splice_func, get_input, depend, set_input, get_issues, [
+ ...path,
+ prop
+ ]);
+ }
+
+ if (prop === 'sort') {
+ const sort_func = function (/** @type {function} */ fn) {
+ const value = get_value();
+ const output = value.sort(fn);
+ set_input(path, value);
+ return output;
+ };
+ return create_field_proxy(sort_func, get_input, depend, set_input, get_issues, [
+ ...path,
+ prop
+ ]);
+ }
+
if (prop === 'value') {
return create_field_proxy(get_value, get_input, depend, set_input, get_issues, [
...path,
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/arrays/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/form/arrays/+page.svelte
new file mode 100644
index 000000000000..fff27c5db7c7
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/remote/form/arrays/+page.svelte
@@ -0,0 +1,51 @@
+
+
+
+
+Full Form Value
+{JSON.stringify(my_form.fields.value(), null, 2)}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/arrays/form.remote.ts b/packages/kit/test/apps/basics/src/routes/remote/form/arrays/form.remote.ts
new file mode 100644
index 000000000000..a9e40538529c
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/remote/form/arrays/form.remote.ts
@@ -0,0 +1,18 @@
+import { form } from '$app/server';
+import * as v from 'valibot';
+
+export const my_form = form(
+ v.object({
+ strings: v.array(v.string()),
+ numbers: v.array(v.number()),
+ objects: v.array(
+ v.object({
+ name: v.string(),
+ age: v.number()
+ })
+ )
+ }),
+ (input) => {
+ return { success: true, data: input };
+ }
+);
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 1e64ded04982..c924c648a0d2 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1,5 +1,5 @@
-import process from 'node:process';
import { expect } from '@playwright/test';
+import process from 'node:process';
import { test } from '../../../utils.js';
/** @typedef {import('@playwright/test').Response} Response */
@@ -1886,6 +1886,19 @@ test.describe('remote functions', () => {
await expect(page.locator('input[name="_password"]')).toHaveValue('');
});
+ test('form field arrays', async ({ page, javaScriptEnabled }) => {
+ if (!javaScriptEnabled) return;
+
+ await page.goto('/remote/form/arrays');
+
+ const value = await page.locator('#full-value').textContent();
+ expect(JSON.parse(value)).toEqual({
+ strings: [''],
+ numbers: [0],
+ objects: [{ name: '', age: 0 }]
+ });
+ });
+
test('prerendered entries not called in prod', async ({ page, clicknav }) => {
await page.goto('/remote/prerender');
await clicknav('[href="/remote/prerender/whole-page"]');
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index d131766a3d12..80cdcdcb9489 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -1871,29 +1871,45 @@ declare module '@sveltejs/kit' {
/**
* Form field accessor type that provides name(), value(), and issues() methods
*/
- export type RemoteFormField = RemoteFormFieldMethods & {
- /**
- * Returns an object that can be spread onto an input element with the correct type attribute,
- * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
- * @example
- * ```svelte
- *
- *
- *
- * ```
- */
- as>(...args: AsArgs): InputElementProps;
- };
+ export type RemoteFormField = RemoteFormFieldMethods &
+ RemoteFormArrayFieldMethods & {
+ /**
+ * Returns an object that can be spread onto an input element with the correct type attribute,
+ * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
+ * @example
+ * ```svelte
+ *
+ *
+ *
+ * ```
+ */
+ as>(...args: AsArgs): InputElementProps;
+ };
- type RemoteFormFieldContainer = RemoteFormFieldMethods & {
- /** Validation issues belonging to this or any of the fields that belong to it, if any */
- allIssues(): RemoteFormIssue[] | undefined;
- };
+ type RemoteFormArrayFieldMethods =
+ T extends Array
+ ? {
+ push(item: U): void;
+ pop(): U;
+ unshift(item: U): void;
+ shift(): U;
+ splice(start: number, deleteCount: number, ...insert: U[]): U[];
+ sort(fn: (a: U, b: U) => number): U[];
+ // you could add more, but the mutate fns are all we really need.
+ // the rest could be accessed like `.value().slice(...)` or `.value().at(-1)`
+ }
+ : {};
+
+ type RemoteFormFieldContainer = RemoteFormFieldMethods &
+ RemoteFormArrayFieldMethods & {
+ /** Validation issues belonging to this or any of the fields that belong to it, if any */
+ allIssues(): RemoteFormIssue[] | undefined;
+ };
/**
* Recursive type to build form fields structure with proxy access
*/
- type RemoteFormFields =
+ export type RemoteFormFields =
WillRecurseIndefinitely extends true
? RecursiveFormFields
: NonNullable extends string | number | boolean | File