sveltekit-superforms
sveltekit-superforms copied to clipboard
SPA onUpdate() doesn't show errors set by setError() on subsequent submits
- [x] Before posting an issue, read the FAQ and search the previous issues.
Description
In SPA mode, the first time the form is submitted and onUpdate() is invoked, any additional errors added by setError() are successfully displayed.
When a field is further modified, the errors are gone (which is expected).
However, when the form is submitted a 2nd/3rd time..., and the form still contains errors such that setError() is called again in onUpdate(), the errors are no longer displayed.
Below is an example src/routes/login/+page.svelte that suffers from this. I can see the API call still being made, so onUpdate() is definitely still invoked on subsequent submits. Hence, the issue is more likely with setError()
<script lang="ts">
import * as Form from '$lib/components/ui/form';
import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { superForm, defaults, setError } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { loginFormSchema, type LoginFormSchema } from './schema';
import SuperDebug from 'sveltekit-superforms';
import { getProfileClient } from '$lib/connect/profile-client.svelte';
import { Code, ConnectError } from '@connectrpc/connect';
import { BadRequestSchema } from '$lib/protogen/google/rpc/error_details_pb';
import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner';
import * as Alert from '$lib/components/ui/alert';
import CircleAlert from 'lucide-svelte/icons/circle-alert';
const profileClient = getProfileClient();
const form = superForm(defaults(zod(loginFormSchema)), {
SPA: true,
validators: zod(loginFormSchema),
onUpdate: async ({ form, result }) => {
if (form.valid) {
const { email, password } = form.data;
try {
await profileClient.login({
email,
password
});
toast.success('You have successfully logged in!');
// navigate the user to the home page with a flash message.
return goto('/');
} catch (err) {
result.type = 'failure';
const connectErr = ConnectError.from(err);
const errCode = connectErr.code;
// TODO: log errCode and errMsg in Sentry.
// const errMsg = connectErr.rawMessage;
switch (errCode) {
case Code.InvalidArgument: {
connectErr.findDetails(BadRequestSchema).find((i) => {
const violations = i.fieldViolations;
for (const violation of violations) {
const field = violation.field;
const message = violation.description;
let formField: '' | LoginFormSchema = '';
switch (field) {
case 'email':
case 'password':
formField = field;
break;
default:
break;
}
setError(form, formField, message);
}
result.status = 400;
});
break;
}
case Code.NotFound:
result.status = 404;
setError(form, 'No account found with that email address');
break;
case Code.Unauthenticated:
result.status = 401; // Unauthorized
setError(form, 'Email or password is incorrect');
break;
default:
result.status = 500;
setError(form, 'An unexpected error occurred when creating your account');
}
}
}
}
});
const { form: formData, errors, allErrors, enhance } = form;
let hasFormLevelErrors = $derived($errors._errors && $errors._errors.length > 0);
let hasFormErrors = $derived($allErrors.length > 0);
</script>
<SuperDebug data={$formData} />
<div class="flex min-h-screen flex-col">
{#if hasFormLevelErrors}
<Alert.Root variant="destructive" class="mx-auto mb-1 mt-auto max-w-sm">
<CircleAlert class="size-4" />
<Alert.Title>Error</Alert.Title>
<Alert.Description>
<ul>
{#each $errors._errors || [] as error}
<li>{error}</li>
{/each}
</ul>
</Alert.Description>
</Alert.Root>
{/if}
<Card.Root class="mx-auto {hasFormLevelErrors ? 'mb-auto mt-1' : 'my-auto'} max-w-sm">
<Card.Header>
<Card.Title class="text-xl">Login</Card.Title>
<Card.Description>Enter your email below to login to your account</Card.Description>
</Card.Header>
<Card.Content>
<div class="grid gap-4">
<form method="POST" use:enhance>
<Form.Field {form} name="email">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Email</Form.Label>
<Input {...props} type="email" bind:value={$formData.email} />
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="password">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Password</Form.Label>
<Input {...props} type="password" bind:value={$formData.password} />
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Button class="mt-4 w-full" disabled={hasFormErrors}>Log in</Form.Button>
</form>
<Form.Button variant="outline" class="w-full">Log in with Google</Form.Button>
</div>
<div class="mt-4 text-center text-sm">
Do not have an account?
<a href="/signup" class="underline"> Sign up </a>
</div>
</Card.Content>
</Card.Root>
</div>
If applicable, a MRE Use this template project to create a minimal reproducible example that you can link to here: https://sveltelab.dev/github.com/ciscoheat/superforms-examples/tree/zod (right click to open in a new tab)
Side note, it seems like result.type = 'failure' is necessary for the errors to be displayed, but it's not mentioned in the documentation.
Also, setError status option (to change the status code) doesn't seem to take any effect. I have to set result.status = ... explicitly (as seen in the example above). However, none of these are blockers, just thought you should know
Are you on the latest SvelteKit? This has just been fixed: https://github.com/ciscoheat/sveltekit-superforms/issues/536
@ciscoheat thanks! updating to 2.15.2 indeed fixed it. And it looks like I no longer need to set result.type = 'failure' anymore which's great.
However, setError() status option still doesn't seem like it's working as expected. It should be 404 here instead of 200. To make it 404, I still have to write result.status = 404 explicitly.
I don't think this has big impact to SPA though, just a bit confusing if someone is debugging things with <SuperDebug/>
Yes, form and result are kind of disconnected in the onUpdate event, so result takes precedence. I'll add a note that you cannot use status in setMessage for SPA.
This is still not working for me on 2.27.1
setError(form, 'password', 'Invalid password' does not work at all. I need to do errors.set({ password: ['Invalid password'] })
Code:
<script lang="ts">
import { authClient } from '$lib/client/auth-client'
import InputWithLabel from '$lib/components/form/InputWithLabel.svelte'
import { Button } from '$lib/components/ui/button'
import * as Form from '$lib/components/ui/form'
import { passwordSchema } from '$lib/formSchemas'
import { defaults, superForm } from 'sveltekit-superforms'
import { zod4 } from 'sveltekit-superforms/adapters'
import { z } from 'zod/v4'
const schema = z.object({
password: passwordSchema,
})
const validator = zod4(schema)
const form = superForm(defaults(validator), {
validators: validator,
SPA: true,
onUpdate: async ({ form }) => {
if (form.valid) {
const result = await authClient.twoFactor.enable({ password: form.data.password })
if (result.error) {
console.log(result.error)
if (result.error.code === 'INVALID_PASSWORD') {
console.log('here')
// setError(form, 'password', 'Invalid password')
errors.set({ password: ['Invalid password'] })
return form
}
} else {
...
}
}
},
})
const { form: formData, enhance, submitting, errors } = form
$inspect($errors)
</script>
<form method="POST" use:enhance class="flex flex-col gap-6">
<InputWithLabel
{form}
bind:value={$formData.password}
name="password"
label="Password"
type="password"
placeholder="Enter your password" />
<div class="flex justify-end gap-2">
<Form.Button submitting={$submitting}>Continue</Form.Button>
</div>
</form>
setError works here: https://www.sveltelab.dev/5v01vy6k37159h6?files=.%2Fsrc%2Froutes%2F%2Bpage.svelte
See if you can figure it out by comparing.
(Also, you don't need to return form in onUpdate)