Search code examples
sveltekit-superforms

Superforms overwrites form elements on update


I'm creating an AI chat application using SvelteKit and Superforms.

The application has an input field for writing a message. When pressing enter, the input field is cleared, and two messages are entered into the conversation: the user's message and a placeholder message "..." that will be replaced by the AI's response when it is ready. While waiting for a response from the AI, the user can start writing another message. But when the form action returns, Superform overwrites whatever the user has entered into the input field while waiting. I think the reason is that the input field uses bind:value={$form.message}, and $form.message is updated when the form action returns.

How can I avoid overwriting the input field?

I've created a StackBlitz showing the issue. To try it, write a message, press enter, and start writing another message without waiting for the AI's response to be ready.

<!-- +page.svelte -->
<script lang="ts">
  import { superForm } from 'sveltekit-superforms/client';

  export let data;

  const { form, enhance } = superForm(data.form, {
    onSubmit: () => {
      data.messages = [
        ...data.messages,
        $form.message,
        "..."
      ]
      // This lines clears the input box on submit.
      $form.message = "";
    }
  });
</script>

<ul>
  {#each data.messages as message}
    <li>{message}</li>
  {/each}
</ul>

<form method="POST" use:enhance>
  <input
    name="message"
    autofocus
    bind:value={$form.message}
    placeholder="Send message"
  />
</form>
// +page.server.ts
import { superValidate } from 'sveltekit-superforms/server';
import { fail } from '@sveltejs/kit';
import { z } from 'zod';
import type { Actions, PageServerLoad } from './$types';

const messages: string[] = [];

const schema = z.object({
  message: z.string().trim().min(1)
});

export const load: PageServerLoad = async () => {
  const form = await superValidate(schema);
  return {
    form,
    messages
  };
};

export const actions: Actions = {
  default: async ({ request }) => {
    const form = await superValidate(request, schema);

    if (!form.valid) return fail(400, { form });

    messages.push(form.data.message);
    await new Promise((resolve) => setTimeout(resolve, 1000));
    messages.push('This is a response');

    // This line prevents populating the input field with the message just sent,
    // but it will overwrite any message the user has written in the meantime.
    form.data.message = "";

    return {
      form,
      messages
    };
  }
};


Solution

  • A solution is to bind the input value to another variable message, and then use a reactive declaration to update $form.message whenever message changes. Clearing form.data.message in +page.server.ts is no longer necessary.

    Here is an updated StackBlitz

    <!-- +page.svelte -->
    <script lang="ts">
      // ...
    
      const { form, enhance } = superForm(data.form, {
        onSubmit: () => {
          data.messages = [
            ...data.messages,
            message,
            "..."
          ]
          // Clear the input box on submit.
          message = "";
        }
      });
    
      // Prevent Superform from overwriting the form element value on form action return.
      let message = $form.message;
      $: $form.message = message;
    </script>
    
    <!-- ... -->
    
    <form method="POST" use:enhance>
      <input
        name="message"
        autofocus
        bind:value={message}
        placeholder="Send message"
      />
    </form>