-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
<Form>
#3533
Comments
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 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 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. |
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
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
The component doesn't know what it contains and it doesn't need to know - |
I'm so glad this is brought up! I have really dug into the Remix What you're proposing seems really sensible to me. For styling, I think you could either expose One quirk I've run into is adding events dynamically on the |
Duh. I thought of exposing mmm...update: it doesn't solve the scoping problem. still have to use |
I've been doing this with my own Edit: |
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 |
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
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.
<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 */)}
>
<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 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. But if you want the benefits of a component, you might consider implementing the Declarative Actions proposal: 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 <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-submitAs 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 Slot propAlso, if there is indeed a <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 |
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:
Of these, persisting state is the 900 lb gorilla, the one I suspect is driving the discussion here about 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 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 |
Also: what should be forwarded to |
@Rich-Harris See Declarative Actions: 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 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: When done this way, Declarative Action will be like a regular component, but with enhancements and without hiding the element attributes. |
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/ |
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:
Btw, this doesn't work if you remove |
Because I'm guessing when |
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 |
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 |
You could also add an |
That's what I meant with "when these bubble up". |
Oh right! Sorry I misread that. |
Cool trick dispatching the custom event! |
I welcome this feature because I believe most people aren't going to bother if it's not easy and if they're not even aware of progressive enhancement because it's not mentioned in the documentation. The SvelteKit documentation is in dire need of an example of a form using progressive enhancement because it's a good idea to seed the idea which Remix has done cementing "Your site should work before JavaScript". As a content creator that made a series on SvelteKit it was weird having to explain progressive enhancement but leaving that responsibility on the user. During my research I could barely find any examples in videos or posts I consumed that take advantage of it because it's not something that's encouraged from the documentation. |
An official Form component could also be a great way to invalidate a page or layout data, or even the session, after a successful submission. It could, for exemple, refresh the session from the server after a login or a user update to re-validate to user data. Right now this is very tedious and hacky in Sveltekit, and I believe this component is at least part of the solution. |
With the There are use cases where you need to control the submit flow. As far as I can see, having a For example, right now I have a use case where a form needs to wait before being actually submitted to the server because an image might be uploading from a component. To accomplish this of course you require total control of the submit flow. |
A form component might also be an opportunity to remove more form and error handling boilerplate from the |
Forms are a major pain, they can easily take up 50% or more of the effort of building an application. The amount of code needed to wire up something as simple as a list of dynamically generated checkboxes using libraries like Formik and react-hook-form is staggering. Doing something to improve this situation could be Svelte and SvelteKit's single biggest win for developers. |
I don't know these libs, but implementing my own solution with forms is really really easy with enchance action. It took me 5 LOCs. |
I'm and end-user type of web developer; the browser and framework internals are above my pay grade, but I make business management apps. My app suite is in need of a rewrite and I'm going with sveltekit and I was really excited about the endpoint feature. Most of what I do starts with grabbing a data table from a database with some sane defaults (like only active employees or only jobs scheduled for today), filtering it down and then viewing/editing a record. Basic business app type stuff, but made as fast as possible so it feels like a native application. I thought surely if there's a feature like endpoints, it must also have simplified re-querying the same endpoint over and over without a page-refreshing form submit, but was disappointed to find that it hasn't. I have it working with code like below, which is fine and not any worse than any other framework but all this is to say that I would just about kill for some tight integration between a component and its associated endpoint, such that I don't have worry about manually building fetch requests in a .svelte file to its matching endpoint. Here's a vote for a custom Form tag with a prop that tells it to hit the JSON endpoint again and do nothing but replace the data without a page refresh. If I'm overlooking a simple way to do this today, please clue me in. What I have:
What I want:
|
@groovy9 You already can have: <script>
import { enhance } from '$lib/form';
export let data = { something: "" };
function addData({ data: _formData, form, response }) {
if (response.ok) {
data = await response.json();
}
form.reset();
}
</script>
<form action="/endpoint" method="POST" use:enhance={{ result: addData }}>
<input type="text" value={data.something} name="something">
<button type="submit">Submit</button>
</form> it works fine with and without JS, and you don't need reload when JS is enabled, it hydrates as you need.... in so hydration is not problem at all... the only problem is CSRF // untested pseudocode
<script lang="ts">
export let data: any[];
let age: number = undefined;
let form
$: if(age) form.submit()
</script>
<Form method="POST" magicProp="updateDataOnly" bind:this={form}>
Age: <input type="text" name="age" bind:value={age} />
</form>
<!-- draw data here --> your example has one big bug... in this case, it would submit right after first input, they would want to write age 36, but it would submit 3, because it would already be true-ish value. |
@Mlocik97, that looked great on the surface, but I'm not sure having an imported custom enhance function that I'll have no memory of in 6 months is less trouble than just composing fetches right in the .svelte files. And anyway, it seems to have some screwy behavior. For example, if I do a form.submit() when a form field updates, it does a full page refresh whereas if I do a submitButton.click() it works as desired. But both of those things should be equivalent, no? Anyway, the moral of the story for me that since this is such an insanely common use case, I'd like the framework to make it dead simple. And sveltekit almost does with endpoints, but stops just a bit short. |
@groovy9 As an aside, You can try using The enhance function works as intended. |
@f-elix ah, yep, that works. And I guess the last mystery is that it always follows up the POST that requestSubmit() does with a GET to the same endpoint, the same one it does on the initial page load. If I change the form action to some other endpoint, it just POSTs that endpoint like I'd expect without the extra GET and otherwise works as desired. I think I'll go ahead and use it even if I have to specify action="/endpoint/__data.json" since I'd have to anyway with fetch. Still pretty clean despite the extra enhance function import. Thanks for the help, everyone.
|
@groovy9 If you want Felte makes it easier to work with forms so you don't have to think about it until it's part of the framework. You might want to read #3532 (comment) If you want to avoid doing |
I like svelte being opinionated and forms are a fundamental part of web development so I'm eager to see some brilliant svelte magic around this topic. |
Was playing around with this a bit, and I feel the component is the way to go, as it allows you to use slot props! <Form
action="/todos"
method="post"
let:state
on:complete={({ detail }) => {
detail.form.reset();
}}
>
<input
class:submitting={state !== 'idle'}
name="text"
aria-label="Add todo"
placeholder="+ tap to add a todo"
/>
</Form> The lack of classing is not really an issue to me, it just generally results in an extra div 🤷 Here is a basic implementation I did in an example project (I am still ignoring a lot of details, including CSRF). It works surprisingly well for the basic use case. I will be improving it and testing more scenarios over time. I will post more findings if I have any. |
Something I have noticed while trying to improve my implementation is that invalidate does not auto cancel duplicate calls. This can lead to very confusing and inconsistent behaviour. It can be even seen now in the current TODOs example when deleting a bunch of records at once. I even accidentally deleted a todo once due to the the page shifting because of this. Should invalidate cancel duplicate requests automagically? |
I chose using <form use:xxx> over <svelte:form> over <Form>. Because it would still work with css libs which apply on <form>. Also it would be nice if <svelte:form> and other <svelte:xxx> got the css applied to <xxx> and <form>. It might seem like this css selection might confuse developers but, if they are using the <svelte:xxx>, it means they've read docs or come across the feature online, where they'll probably be warned of this. Just a suggestion. |
Can we for now expose existing enhance action, so instead of having it in |
Is there any way to use await block when the request is pending with use:enhance when it is client-side rendering? For example:
|
You can do that by creating a promise in the pending function and resolving that Promise in the result. I made a example that uses a similar Pattern. |
CSRFInstead one complex solution is better provide small parts, with the help of which users can compose the desired solution. OT: URL for CSRF in first post is broken (remove |
@bato3 would it work without JS? |
I must note my suggestion Targeted Slots - sveltejs/rfcs#68 I previously cited Declarative Actions, but the author agreed that Targeted Slots solves his problem completely, solving other problems as well. <script>
import { Form } from '$app/navigation';
</script>
<Form
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 */)}
>
<svelte:element slot="form"
action="/api/test?_method=PUT" // manually method overrides
method="post"
class="flex flex-column p-4 mt-2"
id="nativeFormELement"
>
<input name="description" aria-label="Add todo" placeholder="+ tap to add a todo">
</svelte:element>
</Form> On the surface it looks usual, the magic of the proposal happens inside - in Simplified <script>
let form;
</script>
<svelte:element targeted:form this="form" bind:this={form}/>
Where To understand how this is supposed to work, read the Targeted Slots syntax. |
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
Describe the proposed solution
I propose adding a
<Form>
component. By default it would work just like a regular<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:If JS isn't disabled,
<Form>
would submit the result viafetch
, 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 aGET
request to/todos.json
,<Form>
can do this automatically: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:
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......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: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......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: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
The text was updated successfully, but these errors were encountered: