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