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
5 changes: 5 additions & 0 deletions .changeset/pre/as-default-until-edited.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: coerce values typed into remote form fields, and only apply the default given to `.as()` until the field is edited
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ export function form(id) {
}
}

set_nested_value(input, field, value);
set_nested_value(input, field, is_file ? value : coerce_form_value(field.type, value));
} else if (is_file) {
if (DEV && element.multiple) {
throw new Error(
Expand All @@ -554,7 +554,10 @@ export function form(id) {
set_nested_value(
input,
field,
element.type === 'checkbox' && !element.checked ? null : element.value
coerce_form_value(
field.type,
element.type === 'checkbox' && !element.checked ? null : element.value
)
);
}

Expand Down
18 changes: 14 additions & 4 deletions packages/kit/src/runtime/form-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,17 @@ export function create_field_proxy(context, target = {}, path = []) {
}

if (prop === 'as') {
/**
* the field's value, or `fallback` until the field has been edited
* (without a fallback there is nothing to suppress, so `dirty` is not read)
* @param {unknown} [fallback]
*/
const read = (fallback) =>
get_value() ??
(fallback !== undefined && Object.hasOwn(context.get_dirty(), key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually a problem to just read get_dirty? Seems like an overoptimization

@Nic-Polumeyv Nic-Polumeyv Aug 26, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had this on the first push. Figured out that reading get_dirty() subscribes the effect to the dirty = {} reassignment in the attachment, which runs after the select has initialised, so it reruns with value: undefined and select_option clears it. There are some better patterns for this, not really worth implementing, and they would all be more code just to make it a tad bit more read-ably understandable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's fine then

? undefined
: fallback);

/**
* @param {string} type
* @param {unknown} [input_value]
Expand Down Expand Up @@ -801,7 +812,7 @@ export function create_field_proxy(context, target = {}, path = []) {
value: {
enumerable: true,
get() {
return get_value() ?? input_value;
return read(input_value);
}
}
});
Expand Down Expand Up @@ -834,7 +845,7 @@ export function create_field_proxy(context, target = {}, path = []) {
enumerable: true,
get() {
const value = get_value();
if (value == null) return checked;
if (value == null) return read(checked);
if (type === 'radio') return value === input_value;
if (is_array) return /** @type {unknown[]} */ (value).includes(input_value);
return value;
Expand Down Expand Up @@ -897,8 +908,7 @@ export function create_field_proxy(context, target = {}, path = []) {
value: {
enumerable: true,
get() {
const value = get_value() ?? input_value;
return value != null ? String(value) : '';
return String(read(input_value) ?? '');
}
}
});
Expand Down
14 changes: 14 additions & 0 deletions packages/kit/src/runtime/form-utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -850,4 +850,18 @@ describe('create_field_proxy', () => {
expect(cloned.getTime()).toBe(original.getTime());
expect(cloned).not.toBe(original);
});

test('the default given to as() only applies until the field is edited', () => {
const edited = create_field_proxy({
form_id: 'form',
get: () => ({}),
set: () => {},
get_issues: () => ({}),
get_touched: () => ({}),
get_dirty: () => ({ a: true })
});

expect(edited.a.as('number', 3).value).toBe('');
expect(edited.a.as('checkbox', true).checked).toBe(undefined);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
import { edit } from './form.remote.ts';
</script>

<form {...edit}>
<input id="amount" {...edit.fields.amount.as('number', 200)} />
<button id="reset" type="reset">reset</button>
</form>
<p id="value">{JSON.stringify(edit.fields.value())}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { form } from '$app/server';
import * as v from 'valibot';

export const edit = form(v.object({ amount: v.optional(v.number()) }), async () => {});
23 changes: 23 additions & 0 deletions packages/kit/test/apps/async/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,29 @@ test.describe('remote functions', () => {
});
});

test('the default given to .as() only applies until the field is edited', async ({
page,
javaScriptEnabled
}) => {
test.skip(!javaScriptEnabled);

await page.goto('/remote/form/default-until-edited');
const amount = page.locator('#amount');
const value = page.locator('#value');
await expect(amount).toHaveValue('200');

await amount.fill('20');
await expect(value).toHaveText('{"amount":20}');

await amount.press('Backspace');
await amount.press('Backspace');
await expect(amount).toHaveValue('');
await expect(value).toHaveText('{}');

await page.click('#reset');
await expect(amount).toHaveValue('200');
});

test('radio and checkbox inputs keep the current value when only another field changes', async ({
page,
javaScriptEnabled
Expand Down
Loading