Skip to content
Open
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/honest-actors-arrive.md
Original file line number Diff line number Diff line change
@@ -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
43 changes: 31 additions & 12 deletions documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -813,37 +813,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';
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

Expand Down
24 changes: 0 additions & 24 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2031,30 +2031,6 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
get pending(): number;
/** Access form fields using object notation */
fields: Input extends void ? never : 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;
};
};

/**
Expand Down
34 changes: 9 additions & 25 deletions packages/kit/src/runtime/app/server/remote/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,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',
Expand Down Expand Up @@ -198,11 +183,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(__)?.[''];
Expand All @@ -228,6 +208,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', {
Expand All @@ -245,11 +234,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
Expand Down
69 changes: 9 additions & 60 deletions packages/kit/src/runtime/client/remote-functions/form.svelte.js
Original file line number Diff line number Diff line change
Expand Up @@ -437,74 +437,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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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 message = get_message();

Expand All @@ -27,14 +22,13 @@
{/if}

<input {...set_message.fields.message.as('text')} />
<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 />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ export const set_message = form(
v.object({
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') {
Expand All @@ -31,7 +32,11 @@ export const set_message = form(
redirect(303, '/remote');
}

message = data.uppercase === 'true' ? data.message.toUpperCase() : data.message;
if (data.action === 'reverse') {
message = data.message.split('').reverse().join('');
} else {
message = data.uppercase === 'true' ? data.message.toUpperCase() : data.message;
}

if (getRequestEvent().isRemoteRequest) {
const deferred = Promise.withResolvers();
Expand All @@ -43,11 +48,6 @@ export const set_message = form(
}
);

export const set_reverse_message = form(v.object({ message: v.string() }), (data) => {
message = data.message.split('').reverse().join('');
return message;
});

export const resolve_deferreds = form(async () => {
for (const deferred of deferreds) {
deferred.resolve();
Expand Down
7 changes: 3 additions & 4 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1753,22 +1753,21 @@ test.describe('remote functions', () => {
await page.waitForURL('/remote');
});

test('form.buttonProps works', async ({ page, javaScriptEnabled }) => {
test('form multiple submit buttons work', async ({ page, javaScriptEnabled }) => {
await page.goto('/remote/form');

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 }) => {
Expand Down
24 changes: 0 additions & 24 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2007,30 +2007,6 @@ declare module '@sveltejs/kit' {
get pending(): number;
/** Access form fields using object notation */
fields: Input extends void ? never : 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;
};
};

/**
Expand Down