breaking: remove `buttonProps` from experimental remote form functions (#14622)
authorSimon H <5968653+dummdidumm@users.noreply.github.com>
Fri, 9 Jan 2026 15:15:40 +0000 (16:15 +0100)
committerGitHub <noreply@github.com>
Fri, 9 Jan 2026 15:15:40 +0000 (10:15 -0500)
* breaking: remove `buttonProps` from experimental remote form functions

use e.g. `<button {...myForm.fields.action.as('submit', 'register')}>Register</button>` button instead

* fix

* error in dev on buttonProps access

* fix

* fix tests

* tweak

* regenerate

---------

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
.changeset/honest-actors-arrive.md [new file with mode: 0644]
documentation/docs/20-core-concepts/60-remote-functions.md
packages/kit/src/exports/public.d.ts
packages/kit/src/runtime/app/server/remote/form.js
packages/kit/src/runtime/client/remote-functions/form.svelte.js
packages/kit/test/apps/async/src/routes/remote/form/[test_name]/+page.svelte
packages/kit/test/apps/async/src/routes/remote/form/[test_name]/form.remote.ts
packages/kit/test/apps/async/test/test.js
packages/kit/types/index.d.ts

diff --git a/.changeset/honest-actors-arrive.md b/.changeset/honest-actors-arrive.md
new file mode 100644 (file)
index 0000000..17412ab
--- /dev/null
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': minor
+---
+
+breaking: remove `buttonProps` from experimental remote form functions; use e.g. `<button {...myForm.fields.action.as('submit', 'register')}>Register</button>` button instead
index 6bdc4e30d779b1a455ef9e333acf8e89caef5998..7c44424dcb6fd020f515a9a18c395c657c9662fb 100644 (file)
@@ -818,37 +818,56 @@ Some forms may be repeated as part of a list. In this case you can create separa
 {/each}
 ```
 
-### buttonProps
+### Multiple submit buttons
 
-By default, submitting a form will send a request to the URL indicated by the `<form>` element's [`action`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#attributes_for_form_submission) attribute, which in the case of a remote function is a property on the form object generated by SvelteKit.
+It's possible for a `<form>` to have multiple submit buttons. For example, you might have a single form that allows you to log in or register depending on which button was clicked.
 
-It's possible for a `<button>` inside the `<form>` to send the request to a _different_ URL, using the [`formaction`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#formaction) attribute. For example, you might have a single form that allows you to log in or register depending on which button was clicked.
-
-This attribute exists on the `buttonProps` property of a form object:
+To accomplish this, add a field to your schema for the button value, and use `as('submit', value)` to bind it:
 
 ```svelte
 <!--- file: src/routes/login/+page.svelte --->
 <script>
-       import { login, register } from '$lib/auth.remote';
+       import { loginOrRegister } from '$lib/auth';
 </script>
 
-<form {...login}>
+<form {...loginOrRegister}>
        <label>
                Your username
-               <input {...login.fields.username.as('text')} />
+               <input {...loginOrRegister.fields.username.as('text')} />
        </label>
 
        <label>
                Your password
-               <input {...login.fields._password.as('password')} />
+               <input {...loginOrRegister.fields._password.as('password')} />
        </label>
 
-       <button>login</button>
-       <button {...register.buttonProps}>register</button>
+       <button {...loginOrRegister.fields.action.as('submit', 'login')}>login</button>
+       <button {...loginOrRegister.fields.action.as('submit', 'register')}>register</button>
 </form>
 ```
 
-Like the form object itself, `buttonProps` has an `enhance` method for customizing submission behaviour.
+In your form handler, you can check which button was clicked:
+
+```js
+/// file: $lib/auth.js
+import * as v from 'valibot';
+import { form } from '$app/server';
+
+export const loginOrRegister = form(
+       v.object({
+               username: v.string(),
+               _password: v.string(),
+               action: v.picklist(['login', 'register'])
+       }),
+       async ({ username, _password, action }) => {
+               if (action === 'login') {
+                       // handle login
+               } else {
+                       // handle registration
+               }
+       }
+);
+```
 
 ## command
 
index 5aa821429dbef2178326cc2e6789661d3d56de3a..fd41c98481d5b57961ce99649f319b0f8cae1746 100644 (file)
@@ -2078,30 +2078,6 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
        get pending(): number;
        /** Access form fields using object notation */
        fields: RemoteFormFields<Input>;
-       /** Spread this onto a `<button>` or `<input type="submit">` */
-       buttonProps: {
-               type: 'submit';
-               formmethod: 'POST';
-               formaction: string;
-               onclick: (event: Event) => void;
-               /** Use the `enhance` method to influence what happens when the form is submitted. */
-               enhance(
-                       callback: (opts: {
-                               form: HTMLFormElement;
-                               data: Input;
-                               submit: () => Promise<void> & {
-                                       updates: (...queries: Array<RemoteQuery<any> | RemoteQueryOverride>) => Promise<void>;
-                               };
-                       }) => void | Promise<void>
-               ): {
-                       type: 'submit';
-                       formmethod: 'POST';
-                       formaction: string;
-                       onclick: (event: Event) => void;
-               };
-               /** The number of pending submissions */
-               get pending(): number;
-       };
 };
 
 /**
index bf69e0200bce275d64dc0b068ea972b935a2efff..9f71d6e7e126524af65fd3ec9d5b470ec3e4c6f4 100644 (file)
@@ -84,21 +84,6 @@ export function form(validate_or_fn, maybe_fn) {
                        }
                });
 
-               const button_props = {
-                       type: 'submit',
-                       onclick: () => {}
-               };
-
-               Object.defineProperty(button_props, 'enhance', {
-                       value: () => {
-                               return { type: 'submit', formaction: instance.buttonProps.formaction, onclick: () => {} };
-                       }
-               });
-
-               Object.defineProperty(instance, 'buttonProps', {
-                       value: button_props
-               });
-
                /** @type {RemoteInfo} */
                const __ = {
                        type: 'form',
@@ -190,11 +175,6 @@ export function form(validate_or_fn, maybe_fn) {
                        enumerable: true
                });
 
-               Object.defineProperty(button_props, 'formaction', {
-                       get: () => `?/remote=${__.id}`,
-                       enumerable: true
-               });
-
                Object.defineProperty(instance, 'fields', {
                        get() {
                                const data = get_cache(__)?.[''];
@@ -222,6 +202,15 @@ export function form(validate_or_fn, maybe_fn) {
                // TODO 3.0 remove
                if (DEV) {
                        throw_on_old_property_access(instance);
+
+                       Object.defineProperty(instance, 'buttonProps', {
+                               get() {
+                                       throw new Error(
+                                               '`form.buttonProps` has been removed: Instead of `<button {...form.buttonProps}>, use `<button {...form.fields.action.as("submit", "value")}>`.' +
+                                                       ' See the PR for more info: https://github.com/sveltejs/kit/pull/14622'
+                                       );
+                               }
+                       });
                }
 
                Object.defineProperty(instance, 'result', {
@@ -239,11 +228,6 @@ export function form(validate_or_fn, maybe_fn) {
                        get: () => 0
                });
 
-               // On the server, buttonProps.pending is always 0
-               Object.defineProperty(button_props, 'pending', {
-                       get: () => 0
-               });
-
                Object.defineProperty(instance, 'preflight', {
                        // preflight is a noop on the server
                        value: () => instance
index ba47e7ff0099f3f151c0483c91a0d06f9b3b849a..c7461e64894167a87f735feaeffc1f42cc73c9d1 100644 (file)
@@ -425,74 +425,23 @@ export function form(id) {
                        )
                );
 
-               /** @param {Parameters<RemoteForm<any, any>['buttonProps']['enhance']>[0]} callback */
-               const form_action_onclick = (callback) => {
-                       /** @param {Event} event */
-                       return async (event) => {
-                               const target = /** @type {HTMLButtonElement} */ (event.currentTarget);
-                               const form = target.form;
-                               if (!form) return;
-
-                               // Prevent this from firing the form's submit event
-                               event.stopPropagation();
-                               event.preventDefault();
-
-                               const form_data = new FormData(form, target);
-
-                               if (DEV) {
-                                       const enctype = target.hasAttribute('formenctype')
-                                               ? target.formEnctype
-                                               : clone(form).enctype;
-
-                                       validate_form_data(form_data, enctype);
-                               }
-
-                               await handle_submit(form, form_data, callback);
-                       };
-               };
-
-               /** @type {RemoteForm<any, any>['buttonProps']} */
-               // @ts-expect-error we gotta set enhance as a non-enumerable property
-               const button_props = {
-                       type: 'submit',
-                       formmethod: 'POST',
-                       formaction: action,
-                       onclick: form_action_onclick(({ submit, form }) =>
-                               submit().then(() => {
-                                       if (!issues.$) {
-                                               form.reset();
-                                       }
-                               })
-                       )
-               };
-
-               Object.defineProperty(button_props, 'enhance', {
-                       /** @type {RemoteForm<any, any>['buttonProps']['enhance']} */
-                       value: (callback) => {
-                               return {
-                                       type: 'submit',
-                                       formmethod: 'POST',
-                                       formaction: action,
-                                       onclick: form_action_onclick(callback)
-                               };
-                       }
-               });
-
-               Object.defineProperty(button_props, 'pending', {
-                       get: () => pending_count
-               });
-
                let validate_id = 0;
 
                // TODO 3.0 remove
                if (DEV) {
                        throw_on_old_property_access(instance);
+
+                       Object.defineProperty(instance, 'buttonProps', {
+                               get() {
+                                       throw new Error(
+                                               '`form.buttonProps` has been removed: Instead of `<button {...form.buttonProps}>, use `<button {...form.fields.action.as("submit", "value")}>`.' +
+                                                       ' See the PR for more info: https://github.com/sveltejs/kit/pull/14622'
+                                       );
+                               }
+                       });
                }
 
                Object.defineProperties(instance, {
-                       buttonProps: {
-                               value: button_props
-                       },
                        fields: {
                                get: () =>
                                        create_field_proxy(
index 485cfab34a113fae5ae3ffa73f41f6753f95fac4..7686db39006cc49ec13d756565127e49e07bd23f 100644 (file)
@@ -1,10 +1,5 @@
 <script>
-       import {
-               get_message,
-               set_message,
-               resolve_deferreds,
-               set_reverse_message
-       } from './form.remote.js';
+       import { get_message, set_message, resolve_deferreds } from './form.remote.js';
 
        const { params } = $props();
 
 
        <input {...set_message.fields.message.as('text')} />
        <input {...set_message.fields.test_name.as('hidden', params.test_name)} />
-       <!--
-        NOTE: there really probably should be a `set_reverse_message' test_name hidden field here, but it collides with the one above.
-        This kind of lines up with our discussions from earlier where we were talking about needing to include the RF hash in the field name.
-        If we do that and this test starts failing, all we'll need to do is add the hidden field back in.
-       -->
-       <button>set message</button>
-       <button {...set_reverse_message.buttonProps}>set reverse message</button>
+
+       <button {...set_message.fields.action.as('submit', 'normal')}>set message</button>
+       <button {...set_message.fields.action.as('submit', 'reverse')}>set reverse message</button>
 </form>
 
 <p>set_message.input.message: {set_message.fields.message.value()}</p>
 <p>set_message.pending: {set_message.pending}</p>
 <p>set_message.result: {set_message.result}</p>
-<p>set_reverse_message.result: {set_reverse_message.result}</p>
 
 <hr />
 
index 8a0d860f0a2f8f9ffd3bfd74dbcdaa88930a7d20..993962048e1bc2c8aa5820dccb5c03b4667a6147 100644 (file)
@@ -16,10 +16,11 @@ export const set_message = form(
                test_name: v.string(),
                id: v.optional(v.string()),
                message: v.picklist(
-                       ['hello', 'goodbye', 'unexpected error', 'expected error', 'redirect'],
+                       ['hello', 'goodbye', 'unexpected error', 'expected error', 'redirect', 'backwards'],
                        'message is invalid'
                ),
-               uppercase: v.optional(v.string())
+               uppercase: v.optional(v.string()),
+               action: v.optional(v.picklist(['normal', 'reverse']))
        }),
        async (data) => {
                if (data.message === 'unexpected error') {
@@ -37,7 +38,11 @@ export const set_message = form(
                const instance = instances.get(data.test_name) ?? { message: 'initial', deferreds: [] };
                instances.set(data.test_name, instance);
 
-               instance.message = data.uppercase === 'true' ? data.message.toUpperCase() : data.message;
+               if (data.action === 'reverse') {
+                       instance.message = data.message.split('').reverse().join('');
+               } else {
+                       instance.message = data.uppercase === 'true' ? data.message.toUpperCase() : data.message;
+               }
 
                if (getRequestEvent().isRemoteRequest) {
                        const deferred = Promise.withResolvers<void>();
index bf647f6e4e38ae9ce2bbcca4be6d720ef0ea5505..01b2f937d3c2c6dd82e11e9622d177f2ce5ee1d1 100644 (file)
@@ -135,22 +135,21 @@ test.describe('remote functions', () => {
                await page.waitForURL('/remote');
        });
 
-       test('form.buttonProps works', async ({ page, javaScriptEnabled }) => {
-               await page.goto('/remote/form/button-props');
+       test('form multiple submit buttons work', async ({ page, javaScriptEnabled }) => {
+               await page.goto('/remote/form/multiple-submit');
 
                await page.fill('[data-unscoped] input', 'backwards');
                await page.getByText('set reverse message').click();
 
                if (javaScriptEnabled) {
+                       await page.getByText('resolve deferreds').click();
                        await page.getByText('message.current: sdrawkcab').waitFor();
                        await expect(page.getByText('await get_message():')).toHaveText(
                                'await get_message(): sdrawkcab'
                        );
                }
 
-               await expect(page.getByText('set_reverse_message.result')).toHaveText(
-                       'set_reverse_message.result: sdrawkcab'
-               );
+               await expect(page.getByText('set_message.result')).toHaveText('set_message.result: sdrawkcab');
        });
 
        test('form scoping with for(...) works', async ({ page, javaScriptEnabled }) => {
index 0cd1dc26a3e81910880e13680e213266bb5be10e..2009117d5e80795c12a56dc738ee91a34d5e2a03 100644 (file)
@@ -2054,30 +2054,6 @@ declare module '@sveltejs/kit' {
                get pending(): number;
                /** Access form fields using object notation */
                fields: RemoteFormFields<Input>;
-               /** Spread this onto a `<button>` or `<input type="submit">` */
-               buttonProps: {
-                       type: 'submit';
-                       formmethod: 'POST';
-                       formaction: string;
-                       onclick: (event: Event) => void;
-                       /** Use the `enhance` method to influence what happens when the form is submitted. */
-                       enhance(
-                               callback: (opts: {
-                                       form: HTMLFormElement;
-                                       data: Input;
-                                       submit: () => Promise<void> & {
-                                               updates: (...queries: Array<RemoteQuery<any> | RemoteQueryOverride>) => Promise<void>;
-                                       };
-                               }) => void | Promise<void>
-                       ): {
-                               type: 'submit';
-                               formmethod: 'POST';
-                               formaction: string;
-                               onclick: (event: Event) => void;
-                       };
-                       /** The number of pending submissions */
-                       get pending(): number;
-               };
        };
 
        /**