Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/blue-news-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add array mutation methods to field arrays
24 changes: 24 additions & 0 deletions documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,30 @@ You can update a field (or a collection of fields) via the `set(...)` method:
</script>
```

### Mutating field arrays

Field arrays have the following extra methods to mutate the array: `.pop()`, `.push()`, `.shift()`, `.unshift()`, `.splice()`, and `.sort()`. Other array methods and properties can still be accessed through the `.value()` method.

```svelte
<script>
import { cart } from '../data.remote';

// add to cart
cart.fields.items.push({
sku: "SKU12345",
name: "TV Set",
price: 1200,
quantity: 1
});

// remove cart item at index `i`
cart.fields.items.splice(i, 1);

// cart total
const total = cart.fields.items.value().reduce((acc, item) => acc + item.price * item.quantity, 0);
</script>
```

### Handling sensitive data

In the case of a non-progressively-enhanced form submission (i.e. where JavaScript is unavailable, for whatever reason) `value()` is also populated if the submitted data is invalid, so that the user does not need to fill the entire form out from scratch.
Expand Down
52 changes: 34 additions & 18 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1895,29 +1895,45 @@ type AsArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
/**
* Form field accessor type that provides name(), value(), and issues() methods
*/
export type RemoteFormField<Value extends RemoteFormFieldValue> = RemoteFormFieldMethods<Value> & {
/**
* 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
* <input {...myForm.fields.myString.as('text')} />
* <input {...myForm.fields.myNumber.as('number')} />
* <input {...myForm.fields.myBoolean.as('checkbox')} />
* ```
*/
as<T extends RemoteFormFieldType<Value>>(...args: AsArgs<T, Value>): InputElementProps<T>;
};
export type RemoteFormField<Value extends RemoteFormFieldValue> = RemoteFormFieldMethods<Value> &
RemoteFormArrayFieldMethods<Value> & {
/**
* 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
* <input {...myForm.fields.myString.as('text')} />
* <input {...myForm.fields.myNumber.as('number')} />
* <input {...myForm.fields.myBoolean.as('checkbox')} />
* ```
*/
as<T extends RemoteFormFieldType<Value>>(...args: AsArgs<T, Value>): InputElementProps<T>;
};

type RemoteFormFieldContainer<Value> = RemoteFormFieldMethods<Value> & {
/** Validation issues belonging to this or any of the fields that belong to it, if any */
allIssues(): RemoteFormIssue[] | undefined;
};
type RemoteFormArrayFieldMethods<T> =
T extends Array<infer U>
? {
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<Value> = RemoteFormFieldMethods<Value> &
RemoteFormArrayFieldMethods<Value> & {
/** 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<T> =
export type RemoteFormFields<T> =
WillRecurseIndefinitely<T> extends true
? RecursiveFormFields
: NonNullable<T> extends string | number | boolean | File
Expand Down
76 changes: 76 additions & 0 deletions packages/kit/src/runtime/form-utils.svelte.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script>
import { my_form } from './form.remote.js';

my_form.fields.set({
strings: [],
numbers: [],
objects: []
});

my_form.fields.strings.push('');
my_form.fields.numbers.push(0);
my_form.fields.objects.push({ name: '', age: 0 });
</script>

<form {...my_form}>
{#each my_form.fields.strings.value() as _, i (i)}
<div>
<label>
String {i + 1}
<input {...my_form.fields.strings[i].as('text')} />
</label>
</div>
{/each}

{#each my_form.fields.numbers.value() as _, i (i)}
<div>
<label>
Number {i + 1}
<input {...my_form.fields.numbers[i].as('number')} />
</label>
</div>
{/each}

{#each my_form.fields.objects.value() as _, i (i)}
<div>
<label>
Object {i + 1} Name
<input {...my_form.fields.objects[i].name.as('text')} />
</label>
<label>
Object {i + 1} Age
<input {...my_form.fields.objects[i].age.as('number')} />
</label>
</div>
{/each}

<button>Submit</button>
</form>

<h2>Full Form Value</h2>
<pre id="full-value">{JSON.stringify(my_form.fields.value(), null, 2)}</pre>
Original file line number Diff line number Diff line change
@@ -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 };
}
);
15 changes: 14 additions & 1 deletion packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -1790,7 +1790,7 @@
await expect(page.locator('[data-scoped] input')).toHaveValue('');
});

test('form enhance(...) works', async ({ page, javaScriptEnabled }) => {

Check warning on line 1793 in packages/kit/test/apps/basics/test/test.js

View workflow job for this annotation

GitHub Actions / test-kit (18, ubuntu-latest, chromium)

flaky test: form enhance(...) works

retries: 2
await page.goto('/remote/form');

await page.fill('[data-enhanced] input', 'hello');
Expand Down Expand Up @@ -1886,6 +1886,19 @@
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"]');
Expand Down
52 changes: 34 additions & 18 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1871,29 +1871,45 @@ declare module '@sveltejs/kit' {
/**
* Form field accessor type that provides name(), value(), and issues() methods
*/
export type RemoteFormField<Value extends RemoteFormFieldValue> = RemoteFormFieldMethods<Value> & {
/**
* 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
* <input {...myForm.fields.myString.as('text')} />
* <input {...myForm.fields.myNumber.as('number')} />
* <input {...myForm.fields.myBoolean.as('checkbox')} />
* ```
*/
as<T extends RemoteFormFieldType<Value>>(...args: AsArgs<T, Value>): InputElementProps<T>;
};
export type RemoteFormField<Value extends RemoteFormFieldValue> = RemoteFormFieldMethods<Value> &
RemoteFormArrayFieldMethods<Value> & {
/**
* 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
* <input {...myForm.fields.myString.as('text')} />
* <input {...myForm.fields.myNumber.as('number')} />
* <input {...myForm.fields.myBoolean.as('checkbox')} />
* ```
*/
as<T extends RemoteFormFieldType<Value>>(...args: AsArgs<T, Value>): InputElementProps<T>;
};

type RemoteFormFieldContainer<Value> = RemoteFormFieldMethods<Value> & {
/** Validation issues belonging to this or any of the fields that belong to it, if any */
allIssues(): RemoteFormIssue[] | undefined;
};
type RemoteFormArrayFieldMethods<T> =
T extends Array<infer U>
? {
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<Value> = RemoteFormFieldMethods<Value> &
RemoteFormArrayFieldMethods<Value> & {
/** 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<T> =
export type RemoteFormFields<T> =
WillRecurseIndefinitely<T> extends true
? RecursiveFormFields
: NonNullable<T> extends string | number | boolean | File
Expand Down
Loading