Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
    };
  }
};

like image 512
Magnar Myrtveit Avatar asked Mar 24 '26 01:03

Magnar Myrtveit


1 Answers

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>
like image 150
Magnar Myrtveit Avatar answered Mar 26 '26 15:03

Magnar Myrtveit