Skip to content
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

Closed
Rich-Harris opened this issue Jan 24, 2022 · 52 comments · Fixed by #6469
Closed

<Form> #3533

Rich-Harris opened this issue Jan 24, 2022 · 52 comments · Fixed by #6469
Labels
breaking change feature request New feature or request size:large significant feature with tricky design questions and multi-day implementation
Milestone

Comments

@Rich-Harris
Copy link
Member

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

  1. was a first-class part of the framework
  2. enabled best practices and best UX
  3. 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:

image

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

@kevmodrome
Copy link

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.

@dummdidumm
Copy link
Member

dummdidumm commented Jan 25, 2022

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).

@kevmodrome
Copy link

kevmodrome commented Jan 25, 2022

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.

@Rich-Harris
Copy link
Member Author

Rich-Harris commented Jan 25, 2022

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

@f-elix
Copy link
Contributor

f-elix commented Jan 25, 2022

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.

@arxpoetica
Copy link
Member

arxpoetica commented Jan 25, 2022

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

@ghost
Copy link

ghost commented Jan 27, 2022

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.

@benmccann benmccann added this to the 1.0 milestone Jan 27, 2022
@oliie
Copy link

oliie commented Jan 28, 2022

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?

@brittneypostma
Copy link
Contributor

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.

@dangelomedinag
Copy link

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.

@PierBover
Copy link

PierBover commented Jan 28, 2022

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?

@lukaszpolowczyk
Copy link
Contributor

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:
sveltejs/rfcs#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.

@lukaszpolowczyk
Copy link
Contributor

@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.

@Glench
Copy link

Glench commented Jan 29, 2022

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.

@cdcarson
Copy link
Contributor

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.

@Rich-Harris
Copy link
Member Author

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.

@Tropix126
Copy link

Tropix126 commented Feb 2, 2022

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.

@lukaszpolowczyk
Copy link
Contributor

@Rich-Harris See Declarative Actions:
sveltejs/rfcs#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:
sveltejs/rfcs#41 (comment)
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?

@arxpoetica
Copy link
Member

@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.

@yuliankarapetkov
Copy link

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:

Check REPL

<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?

@PierBover
Copy link

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.

@yuliankarapetkov
Copy link

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.

@PierBover
Copy link

PierBover commented Feb 10, 2022

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.

@kevmodrome
Copy link

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

@f-elix
Copy link
Contributor

f-elix commented Feb 10, 2022

A better approach is to use an action on the

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 component Rich is proposing here, I don't know how that would work since actions cannot be used on components.

You could also add an input or change listener (or both) on the form and use event delegation to get the values.

@PierBover
Copy link

You could also add an input or change listener (or both) on the form and use event delegation to get the values.

That's what I meant with "when these bubble up".

@f-elix
Copy link
Contributor

f-elix commented Feb 10, 2022

Oh right! Sorry I misread that.

@PierBover
Copy link

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!

@mattcroat
Copy link
Contributor

mattcroat commented Mar 8, 2022

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.

@f-elix
Copy link
Contributor

f-elix commented Mar 8, 2022

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.

@PierBover
Copy link

With the <Form> component would there be an escape hatch that isn't "figure it out all yourself"?

There are use cases where you need to control the submit flow. As far as I can see, having a <Form> would completely remove this control. The escape hatch would be using a regular <form> element and negate all the benefits of the <Form> component.

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.

@ITwrx
Copy link

ITwrx commented Mar 9, 2022

A form component might also be an opportunity to remove more form and error handling boilerplate from the .svelte and .js/.ts endpoint files. Maybe it could transparently invalidate a Store associated with a form when appropriate too? I haven't used Stores yet, so maybe that is ill-conceived.

@benmccann benmccann added the feature request New feature or request label Mar 17, 2022
@tsongas
Copy link

tsongas commented Apr 9, 2022

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.

@Mlocik97
Copy link
Contributor

Mlocik97 commented Apr 9, 2022

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.

@groovy9
Copy link

groovy9 commented Apr 25, 2022

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:

// quick and dirty, don't judge me
<script lang="ts">
  import { browser } from "$app/env";
  export let rows: any[];
  let data: any[] = rows;
  let age: number = undefined;

  $: update(age);

  async function update(age: number) {
    if (browser) {
      let url = `/stuff/__data.json`;
      if (age) url += `?age=${age}`;
      // get and post both work in endpoint but gets are shorter to demo
      const response = await fetch(url);
      let d = await response.json();
      data = d.rows;
    }
  }
</script>

<form>
  Age: <input type="text" name="age" bind:value={age} />
</form>
<!-- draw data here -->

What I want:

// 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 -->

@Mlocik97
Copy link
Contributor

Mlocik97 commented Apr 26, 2022

@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 $lib/form you have: https://github.com/sveltejs/kit/blob/master/packages/create-svelte/templates/default/src/lib/form.ts

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.

@groovy9
Copy link

groovy9 commented Apr 26, 2022

@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.

@f-elix
Copy link
Contributor

f-elix commented Apr 26, 2022

@groovy9 As an aside, form.submit() and submitButton.click() does not work the same. The full page refresh that occurs when you call form.submit() is the normal behavior of that method.

You can try using form.requestSubmit() instead (https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit).

The enhance function works as intended.

@groovy9
Copy link

groovy9 commented Apr 26, 2022

@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.

// endpoint.svelte
<script lang="ts">
  export let rows: any[]; 
  let data: any[] = rows; 
  let form
  ...
  $: resubmit(age)

  function resubmit(age) {
    form?.requestSubmit()  // triggers POST plus extra GET to /endpoint/__data.json
  }

  async function addData({ response }) {
    if (response.ok) {
      let d = await response.json();  // only sees the POST results
      data = d.rows;
    }
  }
</script>

<!-- action="/endpoint"                     causes extra GET
     action="/endpoint/__data.json"         no extra GET
     action="/otherEndpoint"                no extra GET -->
<form method="post" use:enhance={{ result: addData }} bind:this={form}>
  Age: <input type="text" name="age" bind:value={age} />
</form>

@mattcroat
Copy link
Contributor

mattcroat commented Apr 26, 2022

@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 GET again from what it sounds.

@SrGeneroso
Copy link
Contributor

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.
Is true HTML5 has some nice features by itself but it could be much better.

@fezproof
Copy link
Contributor

fezproof commented Jun 3, 2022

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!
I mocked up a quick implementation and it allows you to do some cool stuff like this:

<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.

@fezproof
Copy link
Contributor

fezproof commented Jun 4, 2022

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?

@NoelJacob
Copy link

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.

@Mlocik97
Copy link
Contributor

Mlocik97 commented Jun 26, 2022

Can we for now expose existing enhance action, so instead of having it in $lib/ we could be able to import it from @sveltejs/kit/enhance or something like that?

@ulvidamirli
Copy link

Is there any way to use await block when the request is pending with use:enhance when it is client-side rendering?

For example:

<script>
   export let data;
<script/>

{#await data} 
   some flashing cards visual 
{:then data} 
   cards filled with data 
{/await}

@abdo643-HULK
Copy link

Is there any way to use await block when the request is pending with use:enhance when it is client-side rendering?

For example:

<script>
   export let data;
<script/>

{#await data} 
   some flashing cards visual 
{:then data} 
   cards filled with data 
{/await}

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.
https://svelte.dev/repl/26fc3f6e57ad4223ae53f95b30fe4f34?version=3.49.0

@benmccann benmccann added the size:large significant feature with tricky design questions and multi-day implementation label Aug 1, 2022
@bato3
Copy link

bato3 commented Aug 13, 2022

CSRF

Instead one complex solution is better provide small parts, with the help of which users can compose the desired solution.
My proposal

OT: URL for CSRF in first post is broken (remove 3533# from it)

@Mlocik97
Copy link
Contributor

@bato3 would it work without JS?

@lukaszpolowczyk
Copy link
Contributor

lukaszpolowczyk commented Aug 13, 2022

I must note my suggestion Targeted Slots - sveltejs/rfcs#68
Targeted Slots would be ideal for the problem in this thread.

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 Form with Targeted Slots syntax.
Syntax description - sveltejs/rfcs#68

Simplified Form:

<script>
  let form;
</script>
<svelte:element targeted:form this="form" bind:this={form}/>

Where Form, does with the binded form, whatever it wants. Also with its children, like <input/>.

To understand how this is supposed to work, read the Targeted Slots syntax.
It also has more capabilities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change feature request New feature or request size:large significant feature with tricky design questions and multi-day implementation
Projects
None yet
Development

Successfully merging a pull request may close this issue.