question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:107
  • Comments:52 (25 by maintainers)

github_iconTop GitHub Comments

22reactions
Rich-Harriscommented, Feb 1, 2022

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.

20reactions
dangelomedinagcommented, Jan 28, 2022

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Google Forms: Online Form Creator | Google Workspace
Use Google Forms to create online forms and surveys with multiple question types. Analyze results in real-time and from any device.
Read more >
Google Forms: Sign-in
Access Google Forms with a personal Google account or Google Workspace account (for business use).
Read more >
Mobile Forms App | Workflow Management Software
Use our mobile forms app for operational compliance or market execution. Build your perfect process with our low-code/no-code workflow management solutions.
Read more >
Form Definition & Meaning - Merriam-Webster
verb ; 1 · to become formed or shaped. A clot was forming over the cut. ; 2 · to take form :...
Read more >
form - Wiktionary
A thing that gives shape to other things as in a mold. Regularity, beauty, or elegance. (philosophy) The inherent nature of an object;...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found