kit
kit copied to clipboard
`<Form>`
Describe the problem
Best practices around form data are slightly annoying — we want to encourage the use of native <form>
behaviour as far as possible (accessible, works without JS, etc) but the best UX involves optimistic updates, pending states, and client-controlled navigation.
So far our best attempt at squaring this circle is the enhance
action found in the project template. It's a decent start, but aside from being tucked away in an example rather than being a documented part of the framework itself, actions are fundamentally limited in what they can do as they cannot affect SSR (which means that method overriding and CSRF protection will always be relegated to userland).
Ideally we would have a solution that
- was a first-class part of the framework
- enabled best practices and best UX
- worked with SSR
Describe the proposed solution
I propose adding a <Form>
component. By default it would work just like a regular <form>
...
<script>
import { Form } from '$app/navigation';
</script>
<Form action="/todos.json" method="post">
<input name="description" aria-label="Add todo" placeholder="+ tap to add a todo">
</Form>
...but with additional features:
Automatic invalidation
Using the invalidate
API, the form could automatically update the UI upon a successful response. In the example above, using the form with JS disabled would show the endpoint response, meaning the endpoint would typically do something like this:
export async function post({ request, locals }) {
const data = await request.formData();
// write the todo to our database...
await db.write('todos', locals.user, data.get('description'));
// ...then redirect the user back to /todos so they see the updated page
return {
status: 303,
headers: {
location: '/todos'
}
};
}
If JS isn't disabled, <Form>
would submit the result via fetch
, meaning there's no need to redirect back to the page we're currently on. But we do want the page to reflect the new data. Assuming (reasonably) that the page is showing the result of a GET
request to /todos.json
, <Form>
can do this automatically:
// (this is pseudo-code, glossing over some details)
const url = new URL(action, location);
url.searchParams.set(method_override.parameter, method);
const res = await fetch(url, {
method: 'post',
body: new FormData(form)
});
// if the request succeeded, we invalidate the URL, causing
// the page's `load` function to run again, updating the UI
if (res.ok) {
invalidate(action);
}
Optimistic UI/pending states
For some updates it's reasonable to wait for confirmation from the server. For others, it might be better to immediately update the UI, possibly with a pending state of some kind:
<Form
action="/todos.json"
method="post"
pending={({ data }) => {
// we add a new todo object immediately — it will be destroyed
// by one without `pending: true` as soon as the action is invalidated
todos = [...todos, {
done: false,
text: data.get('text'),
pending: true
}];
}}
done={({ form }) => {
// clear the form
form.reset();
}}
>
<input name="description" aria-label="Add todo" placeholder="+ tap to add a todo">
</Form>
{#each todos as todo}
<div class="todo" class:done={todo.done} class:pending={todo.pending}>
<!-- todo goes here -->
</div>
{/each}
<style>
.pending {
opacity: 0.5;
}
</style>
this example glosses over one problem — typically you might generate a UUID on the server and use that in a keyed each block or something. ideally the pending todo would also have a UUID that the server accepted so that the invalidation didn't cause glitches. need to think on that some more
Error handling
There's a few things that could go wrong when submitting a form — network error, 4xx error (e.g. invalid data) or 5xx error (the server blew up). These are currently handled a bit inconsistently. If the handler returns an explicit error code, the page just shows the returned body
, whereas if an error is thrown, SvelteKit renders the error page.
#3532 suggests a way we can improve error handling, by rendering a page with validation errors. For progressively enhanced submissions, this wouldn't quite work — invalidating the action would cause a GET
request to happen, leaving the validation errors in limbo. We can pass them to an event handler easily enough...
<Form
action="/todos.json"
method="post"
pending={({ data }) => {...}}
done={({ form }) => {...}}
error={async ({ response }) => {
({ errors } = await response.json());
}}
/>
...but we don't have a great way to enforce correct error handling. Maybe we don't need to, as long as we provide the tools? Need to think on this some more.
Method overriding
Since the component would have access to the methodOverride
config, it could override the method or error when a disallowed method is used:
<Form action="/todos/{todo.uid}.json" method="patch">
<input aria-label="Edit todo" name="text" value={todo.text} />
<button class="save" aria-label="Save todo" />
</Form>
CSRF
We still need to draw the rest of the owl:

I think <Form>
has an important role to play here though. It could integrate with Kit's hypothetical CSRF config and automatically add a hidden input...
<!-- <Form> implementation -->
<script>
import { csrf } from '$app/env'; // or somewhere
</script>
<form {action} {method} on:submit|preventDefault={handler}>
<input type="hidden" name={csrf.key} value={csrf.value}>
<slot/>
</form>
...which we could then somehow validate on the server. For example — this may be a bit magical, but bear with me — maybe we could intercept request.formData()
and throw an error if CSRF checking (most likely using the double submit technique) fails? We could add some logic to our existing response proxy:
// pseudo-code
const proxy = new Proxy(response, {
get(response, key, _receiver) {
if (key === 'formData') {
const data = await response.formData();
const cookies = cookie.parse(request.headers.get('cookie'));
// check that the CSRF token the page was rendered with
// matches the cookie that was set alongside the page
if (data.get(csrf.key) !== cookies[csrf.cookie]) {
throw new Error('CSRF checking failed');
}
return data;
}
}
// ...
});
This would protect a lot of users against CSRF attacks without app developers really needing to do anything at all. We would need to discourage people from using <Form>
on prerendered pages, of course, which is easy to do during SSR.
Alternatives considered
The alternative is to leave it to userland. I don't think I've presented anything here that requires hooks into the framework proper — everything is using public APIs (real or hypothetical). But I think there's a ton of value in having this be built in, so that using progressively-enhanced form submissions is seen as the right way to handle data.
This is a big proposal with a lot of moving parts, so there are probably a variety of things I haven't considered. Eager to hear people's thoughts.
Importance
would make my life easier
Additional Information
No response
I generally like this idea. I've been using the form action and it's very nice.
How do we handle styling? Using an action it's easy since you just leave it to the user, with a component you need some kind of API.
I like this idea as well. Some things that are not clear yet, they are essentially about how far does "progressive enhancement" go. For example, let's say I want to provide better DX and do client-side validation (like: on blur the field is marked as invalid because it's required and there's no content in it). How to achieve this? Another use case: You have a component library which wraps HTML input elements of different kinds - is it still possible to interact with them in an easy way? An advanced use case: You have a list of form items and you can add more items by clicking a +
button - will the form be able to detect this dynamic addition of another form element?
These are all use cases you don't need for a simple sign up form but which will come up in internal business apps quite often. If we don't support these use cases, this should be made clear in the documentation at least, and that it's possible to just use something else entirely if you know you'll have JS interactivity on the site anyway (closed internal business apps etc).
I like this idea as well. Some things that are not clear yet, they are essentially about how far does "progressive enhancement" go. For example, let's say I want to provide better DX and do client-side validation (like: on blur the field is marked as invalid because it's required and there's no content in it). How to achieve this? Another use case: You have a component library which wraps HTML input elements of different kinds - is it still possible to interact with them in an easy way? An advanced use case: You have a list of form items and you can add more items by clicking a
+
button - will the form be able to detect this dynamic addition of another form element?
I think the correct way to handle form validation is to rely heavily on the platform - nudge users in the direction of using CSS. There's a very thorough MDN article on client-side validation. The advantage of this is that it would work both in js and non-js situations.
Here's a very barebones example of what we might encourage users to do. It could be made even simpler if you just wanted a visual indicator without the error message.
How do we handle styling?
Styles that you would have applied to the form itself probably need to go on a wrapper element either inside or outside the component. I think this is a reasonably small price to pay
For example, let's say I want to provide better DX and do client-side validation (like: on blur the field is marked as invalid because it's required and there's no content in it). How to achieve this?
We could add a presubmit callback with the ability to abort, but honestly most of the things that it's possible to validate client-side (ie not stuff like 'username already taken') can be done with HTML attributes, and we should push people in that direction
You have a component library which wraps HTML input elements of different kinds - is it still possible to interact with them in an easy way? An advanced use case: You have a list of form items and you can add more items by clicking a + button - will the form be able to detect this dynamic addition of another form element?
The component doesn't know what it contains and it doesn't need to know - new FormData(form)
only cares what's in the DOM at submit time
I'm so glad this is brought up! I have really dug into the Remix <Form>
component recently and implemented a version of it in Sveltekit (not a package, not yet at least). As things are right now I have ro resort to some hacks (like passing action data to event.locals
and populating the session
with it to retrieve it in my components), so I would absolutely love if Sveltekit provided an official solution.
What you're proposing seems really sensible to me.
For styling, I think you could either expose class
and style
props, or juste use $$restProps
and allow any attribute so they can be used for styling (like data attributes and such). Maybe there are problems I'm not thinking of with such a blunt approach though.
One quirk I've run into is adding events dynamically on the form
itself, like a reset
event, or input
/change
events (so we can use event delegation and not add a listener on every field). Right now I'm forwarding the native events that I want to use from the form because Svelte doesn't support dynamic events. That is of course more related to Svelte than Sveltekit, but I think its worth mentioning, since its a use case a lot of people might run into.
For styling, I think you could either expose class and style props
Duh. I thought of exposing style
(and hated just that), but exposing both solves the styling problem. Do that @Rich-Harris and I'll stop complaining. 😉
mmm...update: it doesn't solve the scoping problem. still have to use :global(){}
or whatever to reach that darn form element
I've been doing this with my own <Form>
component for a while and think it makes form handling a lot easier. I've chosen to handle validation with JS though so that the same validate
function can be called programmatically on the client, on blur and on the server. Not 100% sure about this but it makes it possible to validate the response a second time on the server and if JS is disabled in the browser the error messages are simply included in the document, server rendered and sent back to the client.
Edit:
I want to add that I'm also not convinced this needs to be added to Svelte Kit itself. It's something you can also include in your UI library. And depending on the project you are working on and the server API I imagine you might even want to add specific changes to the <Form>
component.
I also like this idea! An other idea is to maybe have it build in already instead of importing Form, and maybe use something like <svelte:form>
instead?
How do we handle styling?
Styles that you would have applied to the form itself probably need to go on a wrapper element either inside or outside the component. I think this is a reasonably small price to pay
There could be props created on the form component to allow for custom styling also or passing them via style props.
I'm not entirely sure that a <Form>
component is the right solution. The reasons for thinking this are the following:
- Svelte/Sveltekit does not provide components like other frameworks eg.
<Link>
, on the other hand, implements a solution directly on the HTML element. (and that's awesome). - It is highly likely that the solution of creating a
<Form>
component will not fit the needs of a large project where you need to manipulate a lot of things, both the element and logic. (you will end up implementing your own solution). - Svelte/Sveltekit it provides enough tools to do it on our own, without making us feel helpless by the framework itself.
- The infinite dilemma of how to apply styles to nested components comes into play. using an "action" maintains the existing advantages (and disadvantages) up to now in terms of styles.
it seems like a better idea to provide an "action" than a component. the difference in implementation will not be too different from one another.
<Form>
component:
<script>
import { Form } from '$app/navigation';
</script>
<Form
action="/api/test"
method="PUT" // method overrides
class="flex flex-column p-4 mt-2"
id="FormComponent"
on:pending={(e) => console.log(e.detail /* => data */)}
on:result={(e) => console.log(e.detail /* => response */)}
on:error={(e) => console.log(e.detail /* => Error handler */)}
>
<form>
with action
<script>
import { formEnhance } from '$app/navigation';
</script>
<form
action="/api/test?_method=PUT" // manually method overrides
method="post"
class="flex flex-column p-4 mt-2"
id="nativeFormELement"
use:formEnhance={{
pending: (data) => console.log(data),
result: async (res) => console.log(await res.json()),
error: (e) => console.log(e.message)
}}
>
PS: my language is Spanish, the text is translated, sorry if there is something strange.
IMO the big advantage of using an action like use:form
is that it would return control of the whole form to the parent component.
No more styling issues but also, what if we want to use custom actions for form validation and whatnot? Or what if we need to get a ref to the form DOM element?
I'm against locking such things into a component. It's not a universal solution.
But if you want the benefits of a component, you might consider implementing the Declarative Actions proposal: https://github.com/sveltejs/rfcs/pull/41
Then you have the benefits of the component, and you don't lose out on the drawbacks of the component, because you still have the full element exposed.
There's magic going on underneath, and it's regular elements on top.
@dangelomedinag Moreover, through use:action
you can create an event listener, then it will look like this:
<script>
import { formEnhance } from '$app/navigation';
</script>
<form
action="/api/test?_method=PUT" // manually method overrides
method="post"
class="flex flex-column p-4 mt-2"
id="nativeFormELement"
on:pending={(e) => console.log(e.detail /* => data */)}
on:result={(e) => console.log(e.detail /* => response */)}
on:error={(e) => console.log(e.detail /* => Error handler */)}
use:formEnhance
>
And through Declarative Actions it will be even easier to design.
Really glad you're thinking about this. In my own SvelteKit projects I've found it very tedious to implement all the form plumbing, and doing it consistently has proven difficult with the result that each page in my app has different ways of doing things.
So it would be a great relief to have one consistent, framework-provided, batteries included solution that is a sensible default, but of course be able to do your own thing if needed.
Pending, pre-submit, post-submit
As for pending updates, pre-submit, and post-submit, one simple solution I've found that handles all of these cases in one is to provide the form's special "do the network request submission stuff" in a function that the user can call:
<script>
function submit({ network_request_magic }) {
// do anything you want before data is sent to the server
const res = await network_request_magic(); // network request stuff happens here
// do anything you want after response is received from the server
}
</script>
<Form action="" on:submit={submit}>
...
</Form>
One problem with this approach is that if the user potentially leaves out a call to network_request_magic
then the form may not work.
Slot prop
Also, if there is indeed a <Form>
component, it would be great to expose the pending network status as a slot prop:
<Form let:submitting={submitting}>
{#if submitting}
<SpinnerThingy />
{/if}
<input disabled={submitting} />
</Form>
I do this in my own Form component and it's great — don't even need to have a <script>
tag.
My take: The javascript-disabled user is an edge case. That doesn't mean that it shouldn't be addressed, just that adressing it should not be central to the SvelteKit API. The approach should be as agnostic and as standard as possible. Ideally, there would be no pre-baked "SvelteKit way".
SvelteKit already handles plain HTML forms just fine. What's needed is documentation and example code that show how to do it. Here are some things that might be addressed:
- Persisting form state from a
POST
across a redirect to a page. - Making client and endpoint code agnostic as to XHR (fetch) vs plain HTML.
- CSRF.
Of these, persisting state is the 900 lb gorilla, the one I suspect is driving the discussion here about <Form/>
and on this thread about shadow endpoints. Maybe I have the wrong end of the stick, but the notion seems to be that a POST
endpoint would both handle the form and automatically return the HTML page, nicely populated with the form state, all in one go.
I understand the attraction of this idea. You don't need to persist & redirect. But in my experience mixing verbs always ends up being fragile, inconvenient, and hard to reason about. It's much better in the long run to persist state and redirect, as complex or unnecessary as that seems in the context of one or two routes.
Net. Dealing with form state and the other concerns relevant to the case is really not that hard and need not be abstracted away by SvelteKit. I'd argue that abstractions like <Form/>
and conventional magic like "shadow endpoints" may pose more of a cognitive hurdle than, say, dealing with Redis or understanding CSRF protection. Further, I'd worry that things end up driving the shape of the overall framework API in service of (what I think should be) an implementation detail best left to userland.
Anyway, if you think the documentation/example route is the way to go I'd be happy to help out.
Re using an action rather than a component — I do understand the appeal, but it does mean adding a ton of boilerplate for CSRF in particular. One appeal of <Form>
is that we could pretty much protect users of SvelteKit apps against CSRF attacks without app developers even needing to know what that means.
Also: what should be forwarded to <Form />
? Should it use $$restProps? What events should be available? There are ways around things like bind:this
(i.e. exporting an element
prop that is bound internally, then using bind:element
in parent context), but I imagine something like that would go against what people are typically used to. The limitations of components are fairly high atm. I do agree that this is a good solution for what it offers, but it should probably have a lot of thought put into it, since it's not something you can really go back on once it's a feature.
@Rich-Harris See Declarative Actions: https://github.com/sveltejs/rfcs/pull/41
With Declarative Actions you can get all the benefits of the Component, but there would be no hidden properties of the HTML element.
Because the problem with use:action
in this case, and in general, is that it is a simple function, not using any of the Component's benefits.
If use:form would be Declarative Action, then the problem disappears.
Declarative Actions is @Zizico2's proposal and here I just (more or less) wrote how a regular Component compares to a Declarative Action:
https://github.com/sveltejs/rfcs/pull/41#issuecomment-934364393
In short: More convenient to use HTML element attributes, need to dynamically pass special attributes to the element inside the component(in my example $$specialProps
, but there is also a forward directive proposal from @Tropix126).
When done this way, Declarative Action will be like a regular component, but with enhancements and without hiding the element attributes. So SSR etc. will be doable with Declarative Action, so I guess CSRF will be just as easy to deal with too?
@Rich-Harris
Re using an action rather than a component — I do understand the appeal, but it does mean adding a ton of boilerplate for CSRF in particular.
Can you illustrate what you mean in terms of code? What might that boilerplate look like?
I admit I'm pretty excited by the idea of actions/use:
instead of locking away the component.
I think the correct way to handle form validation is to rely heavily on the platform - nudge users in the direction of using CSS. There's a very thorough MDN article on client-side validation. The advantage of this is that it would work both in js and non-js situations.
Here's a very barebones example of what we might encourage users to do. It could be made even simpler if you just wanted a visual indicator without the error message.
Although this is not a SvelteKit feature, I believe this information should be added somewhere in the docs. The reason being there are lots of developers who are used to using third-party libraries for client-side validation or having built-in solutions like Angular's Reactive Forms, for example. I didn't even know that the Constraint Validation API existed just before reading your comment.
So I believe having an example like this one in the docs would be helpful for lots of people:
<script>
let input
let value
</script>
<form>
<p>
This doesn't work without using <em>bind:value</em>
</p>
<label for="email">Email:</label>
<input type="email" id="email" minlength="8" bind:this={input} bind:value>
{#if input?.validity.tooShort}
<p>
too short
</p>
{/if}
</form>
<style>
input:invalid {
outline: 1px solid red;
}
</style>
Btw, this doesn't work if you remove bind:value
. Why?
Btw, this doesn't work if you remove bind:value. Why?
Because input.validity
is not reactive.
I'm guessing when value
updates, it forces a render of the if block.
Because
input.validity
is not reactive.I'm guessing when
value
updates, it forces a render of the if block.
Gotcha. So I guess there has to be a working example (unlike mine) in the docs.
Gotcha. So I guess there has to be a working example (unlike mine) in the docs.
It should be trivial to create an action that updates a reactive writable store after the input event. The problem is it becomes tedious for larger forms with many fields.
A better approach is to use an action on the <form>
element which would allow to get the events when these bubble up, or to add event listeners to all inputs (automatically from the action). Unfortunately, with the <Form>
component Rich is proposing here, I don't know how that would work since actions cannot be used on components.
Here's an old library I made once upon a time. It has an action that serialises and deserialises the form data (based on lukeeds excellent library btw!): https://github.com/svelteschool/svelte-forms/blob/master/src/actions/form/getValues.js
A better approach is to use an action on the
You could also add an input
or change
listener (or both) on the form and use event delegation to get the values.
You could also add an
input
orchange
listener (or both) on the form and use event delegation to get the values.
That's what I meant with "when these bubble up".
Oh right! Sorry I misread that.
Here's an old library I made once upon a time. It has an action that serialises and deserialises the form data (based on lukeeds excellent library btw!): https://github.com/svelteschool/svelte-forms/blob/master/src/actions/form/getValues.js
Cool trick dispatching the custom event!
Love the idea of CSRF protection being automatic and default, like in rails.
Can I suggest, though, that it migth be worth giving it a slightly different name than <Form>
, if only for readability's sake?
<Form action="/todos.json" method="post">
<input name="description" aria-label="Add todo" placeholder="+ tap to add a todo">
</Form>
Scanning through a file visually, it's a rather subtle thing to notice that isn't a regular <form>
.
Alternatives might be <svelte:form>
as @oliie suggested, which would be consistent with the other 7 built-in components, or a namespace, e.g. <Svelte.Form>
, or even a two-word <SvelteForm>
would be a lot easier to notice.
Now that I think about forms, I think we don't need <Form>
resp. <svelte:form>
component, as crsf is solvable with just standard HTML forms, and hydratation is solved by Svelte action, so why don't we just that action that is in demo app, make it importable from kit? Also I think it would make code more natural and simpler. I think it's simple do push that one crsf
"inpur" token to formData inside action (E: oh wait it will need to be element as it needs to work without JS, still this can be done (add that hidden csrf element) by compiler I think?)
like in example in Rich's first post. <input type="hidden" name={csrf.key} value={csrf.value}>
and for correctly handling redirects, we can also append ?js=true
param to fetch in action code, so if JS is enabled server knows it's done by fetch API (so it respond with status 200), while if js is false, it will response with status 302 with location user specify in endpoint in case if action points to other URL than actual page is on. (So it doesn't redirects to endpoint URL where action points, because in that case <form action="/api/something/>
would redirect page to that API path URL.
I think if this will be solved this way, it will be soooooo easy to use forms and make it powerfull...