+++ /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}
```
-### Multiple submit buttons
+### buttonProps
-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.
+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.
-To accomplish this, add a field to your schema for the button value, and use `as('submit', value)` to bind it:
+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:
```svelte
<!--- file: src/routes/login/+page.svelte --->
<script>
- import { loginOrRegister } from '$lib/auth';
+ import { login, register } from '$lib/auth.remote';
</script>
-<form {...loginOrRegister}>
+<form {...login}>
<label>
Your username
- <input {...loginOrRegister.fields.username.as('text')} />
+ <input {...login.fields.username.as('text')} />
</label>
<label>
Your password
- <input {...loginOrRegister.fields._password.as('password')} />
+ <input {...login.fields._password.as('password')} />
</label>
- <button {...loginOrRegister.fields.action.as('submit', 'login')}>login</button>
- <button {...loginOrRegister.fields.action.as('submit', 'register')}>register</button>
+ <button>login</button>
+ <button {...register.buttonProps}>register</button>
</form>
```
-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
- }
- }
-);
-```
+Like the form object itself, `buttonProps` has an `enhance` method for customizing submission behaviour.
## 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 } from './form.remote.js';
+ import {
+ get_message,
+ set_message,
+ resolve_deferreds,
+ set_reverse_message
+ } 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)} />
-
- <button {...set_message.fields.action.as('submit', 'normal')}>set message</button>
- <button {...set_message.fields.action.as('submit', 'reverse')}>set reverse message</button>
+ <!--
+ 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>
</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', 'backwards'],
+ ['hello', 'goodbye', 'unexpected error', 'expected error', 'redirect'],
'message is invalid'
),
- uppercase: v.optional(v.string()),
- action: v.optional(v.picklist(['normal', 'reverse']))
+ uppercase: v.optional(v.string())
}),
async (data) => {
if (data.message === 'unexpected error') {
const instance = instances.get(data.test_name) ?? { message: 'initial', deferreds: [] };
instances.set(data.test_name, instance);
- if (data.action === 'reverse') {
- instance.message = data.message.split('').reverse().join('');
- } else {
- instance.message = data.uppercase === 'true' ? data.message.toUpperCase() : data.message;
- }
+ instance.message = data.uppercase === 'true' ? data.message.toUpperCase() : data.message;
if (getRequestEvent().isRemoteRequest) {
const deferred = Promise.withResolvers<void>();
await page.waitForURL('/remote');
});
- test('form multiple submit buttons work', async ({ page, javaScriptEnabled }) => {
- await page.goto('/remote/form/multiple-submit');
+ test('form.buttonProps works', async ({ page, javaScriptEnabled }) => {
+ await page.goto('/remote/form/button-props');
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_message.result')).toHaveText('set_message.result: sdrawkcab');
+ await expect(page.getByText('set_reverse_message.result')).toHaveText(
+ 'set_reverse_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;
+ };
};
/**