--- /dev/null
+---
+'@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
{/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
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;
- };
};
/**
}
});
- 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',
enumerable: true
});
- Object.defineProperty(button_props, 'formaction', {
- get: () => `?/remote=${__.id}`,
- enumerable: true
- });
-
Object.defineProperty(instance, 'fields', {
get() {
const data = get_cache(__)?.[''];
// 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', {
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
)
);
- /** @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(
<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 />
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') {
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>();
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 }) => {
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;
- };
};
/**