Skip to content

Commit 4f9870d

Browse files
breaking: remove buttonProps from experimental remote form functions (sveltejs#14622)
* 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>
1 parent c8e4017 commit 4f9870d

File tree

9 files changed

+70
-167
lines changed

9 files changed

+70
-167
lines changed

.changeset/honest-actors-arrive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
breaking: remove `buttonProps` from experimental remote form functions; use e.g. `<button {...myForm.fields.action.as('submit', 'register')}>Register</button>` button instead

documentation/docs/20-core-concepts/60-remote-functions.md

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -818,37 +818,56 @@ Some forms may be repeated as part of a list. In this case you can create separa
818818
{/each}
819819
```
820820
821-
### buttonProps
821+
### Multiple submit buttons
822822
823-
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.
823+
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.
824824
825-
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.
826-
827-
This attribute exists on the `buttonProps` property of a form object:
825+
To accomplish this, add a field to your schema for the button value, and use `as('submit', value)` to bind it:
828826
829827
```svelte
830828
<!--- file: src/routes/login/+page.svelte --->
831829
<script>
832-
import { login, register } from '$lib/auth.remote';
830+
import { loginOrRegister } from '$lib/auth';
833831
</script>
834832

835-
<form {...login}>
833+
<form {...loginOrRegister}>
836834
<label>
837835
Your username
838-
<input {...login.fields.username.as('text')} />
836+
<input {...loginOrRegister.fields.username.as('text')} />
839837
</label>
840838

841839
<label>
842840
Your password
843-
<input {...login.fields._password.as('password')} />
841+
<input {...loginOrRegister.fields._password.as('password')} />
844842
</label>
845843

846-
<button>login</button>
847-
<button {...register.buttonProps}>register</button>
844+
<button {...loginOrRegister.fields.action.as('submit', 'login')}>login</button>
845+
<button {...loginOrRegister.fields.action.as('submit', 'register')}>register</button>
848846
</form>
849847
```
850848
851-
Like the form object itself, `buttonProps` has an `enhance` method for customizing submission behaviour.
849+
In your form handler, you can check which button was clicked:
850+
851+
```js
852+
/// file: $lib/auth.js
853+
import * as v from 'valibot';
854+
import { form } from '$app/server';
855+
856+
export const loginOrRegister = form(
857+
v.object({
858+
username: v.string(),
859+
_password: v.string(),
860+
action: v.picklist(['login', 'register'])
861+
}),
862+
async ({ username, _password, action }) => {
863+
if (action === 'login') {
864+
// handle login
865+
} else {
866+
// handle registration
867+
}
868+
}
869+
);
870+
```
852871
853872
## command
854873

packages/kit/src/exports/public.d.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2078,30 +2078,6 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
20782078
get pending(): number;
20792079
/** Access form fields using object notation */
20802080
fields: RemoteFormFields<Input>;
2081-
/** Spread this onto a `<button>` or `<input type="submit">` */
2082-
buttonProps: {
2083-
type: 'submit';
2084-
formmethod: 'POST';
2085-
formaction: string;
2086-
onclick: (event: Event) => void;
2087-
/** Use the `enhance` method to influence what happens when the form is submitted. */
2088-
enhance(
2089-
callback: (opts: {
2090-
form: HTMLFormElement;
2091-
data: Input;
2092-
submit: () => Promise<void> & {
2093-
updates: (...queries: Array<RemoteQuery<any> | RemoteQueryOverride>) => Promise<void>;
2094-
};
2095-
}) => void | Promise<void>
2096-
): {
2097-
type: 'submit';
2098-
formmethod: 'POST';
2099-
formaction: string;
2100-
onclick: (event: Event) => void;
2101-
};
2102-
/** The number of pending submissions */
2103-
get pending(): number;
2104-
};
21052081
};
21062082

21072083
/**

packages/kit/src/runtime/app/server/remote/form.js

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,6 @@ export function form(validate_or_fn, maybe_fn) {
8484
}
8585
});
8686

87-
const button_props = {
88-
type: 'submit',
89-
onclick: () => {}
90-
};
91-
92-
Object.defineProperty(button_props, 'enhance', {
93-
value: () => {
94-
return { type: 'submit', formaction: instance.buttonProps.formaction, onclick: () => {} };
95-
}
96-
});
97-
98-
Object.defineProperty(instance, 'buttonProps', {
99-
value: button_props
100-
});
101-
10287
/** @type {RemoteInfo} */
10388
const __ = {
10489
type: 'form',
@@ -190,11 +175,6 @@ export function form(validate_or_fn, maybe_fn) {
190175
enumerable: true
191176
});
192177

193-
Object.defineProperty(button_props, 'formaction', {
194-
get: () => `?/remote=${__.id}`,
195-
enumerable: true
196-
});
197-
198178
Object.defineProperty(instance, 'fields', {
199179
get() {
200180
const data = get_cache(__)?.[''];
@@ -222,6 +202,15 @@ export function form(validate_or_fn, maybe_fn) {
222202
// TODO 3.0 remove
223203
if (DEV) {
224204
throw_on_old_property_access(instance);
205+
206+
Object.defineProperty(instance, 'buttonProps', {
207+
get() {
208+
throw new Error(
209+
'`form.buttonProps` has been removed: Instead of `<button {...form.buttonProps}>, use `<button {...form.fields.action.as("submit", "value")}>`.' +
210+
' See the PR for more info: https://github.com/sveltejs/kit/pull/14622'
211+
);
212+
}
213+
});
225214
}
226215

227216
Object.defineProperty(instance, 'result', {
@@ -239,11 +228,6 @@ export function form(validate_or_fn, maybe_fn) {
239228
get: () => 0
240229
});
241230

242-
// On the server, buttonProps.pending is always 0
243-
Object.defineProperty(button_props, 'pending', {
244-
get: () => 0
245-
});
246-
247231
Object.defineProperty(instance, 'preflight', {
248232
// preflight is a noop on the server
249233
value: () => instance

packages/kit/src/runtime/client/remote-functions/form.svelte.js

Lines changed: 9 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -425,74 +425,23 @@ export function form(id) {
425425
)
426426
);
427427

428-
/** @param {Parameters<RemoteForm<any, any>['buttonProps']['enhance']>[0]} callback */
429-
const form_action_onclick = (callback) => {
430-
/** @param {Event} event */
431-
return async (event) => {
432-
const target = /** @type {HTMLButtonElement} */ (event.currentTarget);
433-
const form = target.form;
434-
if (!form) return;
435-
436-
// Prevent this from firing the form's submit event
437-
event.stopPropagation();
438-
event.preventDefault();
439-
440-
const form_data = new FormData(form, target);
441-
442-
if (DEV) {
443-
const enctype = target.hasAttribute('formenctype')
444-
? target.formEnctype
445-
: clone(form).enctype;
446-
447-
validate_form_data(form_data, enctype);
448-
}
449-
450-
await handle_submit(form, form_data, callback);
451-
};
452-
};
453-
454-
/** @type {RemoteForm<any, any>['buttonProps']} */
455-
// @ts-expect-error we gotta set enhance as a non-enumerable property
456-
const button_props = {
457-
type: 'submit',
458-
formmethod: 'POST',
459-
formaction: action,
460-
onclick: form_action_onclick(({ submit, form }) =>
461-
submit().then(() => {
462-
if (!issues.$) {
463-
form.reset();
464-
}
465-
})
466-
)
467-
};
468-
469-
Object.defineProperty(button_props, 'enhance', {
470-
/** @type {RemoteForm<any, any>['buttonProps']['enhance']} */
471-
value: (callback) => {
472-
return {
473-
type: 'submit',
474-
formmethod: 'POST',
475-
formaction: action,
476-
onclick: form_action_onclick(callback)
477-
};
478-
}
479-
});
480-
481-
Object.defineProperty(button_props, 'pending', {
482-
get: () => pending_count
483-
});
484-
485428
let validate_id = 0;
486429

487430
// TODO 3.0 remove
488431
if (DEV) {
489432
throw_on_old_property_access(instance);
433+
434+
Object.defineProperty(instance, 'buttonProps', {
435+
get() {
436+
throw new Error(
437+
'`form.buttonProps` has been removed: Instead of `<button {...form.buttonProps}>, use `<button {...form.fields.action.as("submit", "value")}>`.' +
438+
' See the PR for more info: https://github.com/sveltejs/kit/pull/14622'
439+
);
440+
}
441+
});
490442
}
491443

492444
Object.defineProperties(instance, {
493-
buttonProps: {
494-
value: button_props
495-
},
496445
fields: {
497446
get: () =>
498447
create_field_proxy(

packages/kit/test/apps/async/src/routes/remote/form/[test_name]/+page.svelte

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
<script>
2-
import {
3-
get_message,
4-
set_message,
5-
resolve_deferreds,
6-
set_reverse_message
7-
} from './form.remote.js';
2+
import { get_message, set_message, resolve_deferreds } from './form.remote.js';
83
94
const { params } = $props();
105
@@ -27,19 +22,14 @@
2722

2823
<input {...set_message.fields.message.as('text')} />
2924
<input {...set_message.fields.test_name.as('hidden', params.test_name)} />
30-
<!--
31-
NOTE: there really probably should be a `set_reverse_message' test_name hidden field here, but it collides with the one above.
32-
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.
33-
If we do that and this test starts failing, all we'll need to do is add the hidden field back in.
34-
-->
35-
<button>set message</button>
36-
<button {...set_reverse_message.buttonProps}>set reverse message</button>
25+
26+
<button {...set_message.fields.action.as('submit', 'normal')}>set message</button>
27+
<button {...set_message.fields.action.as('submit', 'reverse')}>set reverse message</button>
3728
</form>
3829

3930
<p>set_message.input.message: {set_message.fields.message.value()}</p>
4031
<p>set_message.pending: {set_message.pending}</p>
4132
<p>set_message.result: {set_message.result}</p>
42-
<p>set_reverse_message.result: {set_reverse_message.result}</p>
4333

4434
<hr />
4535

packages/kit/test/apps/async/src/routes/remote/form/[test_name]/form.remote.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ export const set_message = form(
1616
test_name: v.string(),
1717
id: v.optional(v.string()),
1818
message: v.picklist(
19-
['hello', 'goodbye', 'unexpected error', 'expected error', 'redirect'],
19+
['hello', 'goodbye', 'unexpected error', 'expected error', 'redirect', 'backwards'],
2020
'message is invalid'
2121
),
22-
uppercase: v.optional(v.string())
22+
uppercase: v.optional(v.string()),
23+
action: v.optional(v.picklist(['normal', 'reverse']))
2324
}),
2425
async (data) => {
2526
if (data.message === 'unexpected error') {
@@ -37,7 +38,11 @@ export const set_message = form(
3738
const instance = instances.get(data.test_name) ?? { message: 'initial', deferreds: [] };
3839
instances.set(data.test_name, instance);
3940

40-
instance.message = data.uppercase === 'true' ? data.message.toUpperCase() : data.message;
41+
if (data.action === 'reverse') {
42+
instance.message = data.message.split('').reverse().join('');
43+
} else {
44+
instance.message = data.uppercase === 'true' ? data.message.toUpperCase() : data.message;
45+
}
4146

4247
if (getRequestEvent().isRemoteRequest) {
4348
const deferred = Promise.withResolvers<void>();

packages/kit/test/apps/async/test/test.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,22 +135,21 @@ test.describe('remote functions', () => {
135135
await page.waitForURL('/remote');
136136
});
137137

138-
test('form.buttonProps works', async ({ page, javaScriptEnabled }) => {
139-
await page.goto('/remote/form/button-props');
138+
test('form multiple submit buttons work', async ({ page, javaScriptEnabled }) => {
139+
await page.goto('/remote/form/multiple-submit');
140140

141141
await page.fill('[data-unscoped] input', 'backwards');
142142
await page.getByText('set reverse message').click();
143143

144144
if (javaScriptEnabled) {
145+
await page.getByText('resolve deferreds').click();
145146
await page.getByText('message.current: sdrawkcab').waitFor();
146147
await expect(page.getByText('await get_message():')).toHaveText(
147148
'await get_message(): sdrawkcab'
148149
);
149150
}
150151

151-
await expect(page.getByText('set_reverse_message.result')).toHaveText(
152-
'set_reverse_message.result: sdrawkcab'
153-
);
152+
await expect(page.getByText('set_message.result')).toHaveText('set_message.result: sdrawkcab');
154153
});
155154

156155
test('form scoping with for(...) works', async ({ page, javaScriptEnabled }) => {

packages/kit/types/index.d.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2054,30 +2054,6 @@ declare module '@sveltejs/kit' {
20542054
get pending(): number;
20552055
/** Access form fields using object notation */
20562056
fields: RemoteFormFields<Input>;
2057-
/** Spread this onto a `<button>` or `<input type="submit">` */
2058-
buttonProps: {
2059-
type: 'submit';
2060-
formmethod: 'POST';
2061-
formaction: string;
2062-
onclick: (event: Event) => void;
2063-
/** Use the `enhance` method to influence what happens when the form is submitted. */
2064-
enhance(
2065-
callback: (opts: {
2066-
form: HTMLFormElement;
2067-
data: Input;
2068-
submit: () => Promise<void> & {
2069-
updates: (...queries: Array<RemoteQuery<any> | RemoteQueryOverride>) => Promise<void>;
2070-
};
2071-
}) => void | Promise<void>
2072-
): {
2073-
type: 'submit';
2074-
formmethod: 'POST';
2075-
formaction: string;
2076-
onclick: (event: Event) => void;
2077-
};
2078-
/** The number of pending submissions */
2079-
get pending(): number;
2080-
};
20812057
};
20822058

20832059
/**

0 commit comments

Comments
 (0)