feat(auth): add use:enhance and server error display to signup form

SignupForm now uses use:enhance for progressive enhancement.
Accepts form prop for server-side error display. Shows general
form errors in a banner and field-specific errors inline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 15:06:21 +02:00
parent bd9e1334e0
commit 6d0f00c8fb
3 changed files with 71 additions and 4 deletions

View File

@@ -1,8 +1,19 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms';
type FormResult = {
errors?: Record<string, string>;
displayName?: string;
email?: string;
} | null;
let { form = null }: { form?: FormResult } = $props();
let displayName = $state(''); let displayName = $state('');
let email = $state(''); let email = $state('');
let password = $state(''); let password = $state('');
let showPassword = $state(false); let showPassword = $state(false);
let formError = $state('');
let errors = $state({ let errors = $state({
displayName: '', displayName: '',
@@ -10,12 +21,24 @@
password: '' password: ''
}); });
function handleSubmit(event: SubmitEvent) { $effect(() => {
if (form?.errors) {
errors.displayName = form.errors.displayName ?? '';
errors.email = form.errors.email ?? '';
errors.password = form.errors.password ?? '';
formError = form.errors.form ?? '';
if (form.displayName) displayName = form.displayName;
if (form.email) email = form.email;
}
});
function validate(): boolean {
let hasError = false; let hasError = false;
errors.displayName = ''; errors.displayName = '';
errors.email = ''; errors.email = '';
errors.password = ''; errors.password = '';
formError = '';
if (!displayName.trim()) { if (!displayName.trim()) {
errors.displayName = 'Name ist erforderlich'; errors.displayName = 'Name ist erforderlich';
@@ -33,13 +56,17 @@
hasError = true; hasError = true;
} }
if (hasError) { return hasError;
}
function handleSubmit(event: SubmitEvent) {
if (validate()) {
event.preventDefault(); event.preventDefault();
} }
} }
</script> </script>
<form method="POST" novalidate onsubmit={handleSubmit}> <form method="POST" novalidate use:enhance onsubmit={handleSubmit}>
<h1 <h1
class="font-[var(--font-display)] text-[18px] font-medium tracking-[-0.02em] text-[var(--color-text)] class="font-[var(--font-display)] text-[18px] font-medium tracking-[-0.02em] text-[var(--color-text)]
md:text-[28px]" md:text-[28px]"
@@ -139,6 +166,12 @@
{/if} {/if}
</div> </div>
{#if formError}
<p class="mb-[16px] rounded-[var(--radius-md)] bg-[color-mix(in_srgb,var(--color-error),transparent_90%)] px-[12px] py-[10px] text-[12px] text-[var(--color-error)]">
{formError}
</p>
{/if}
<!-- Submit button --> <!-- Submit button -->
<button <button
type="submit" type="submit"

View File

@@ -3,6 +3,10 @@ import { render, screen } from '@testing-library/svelte';
import { userEvent } from '@testing-library/user-event'; import { userEvent } from '@testing-library/user-event';
import SignupForm from './SignupForm.svelte'; import SignupForm from './SignupForm.svelte';
vi.mock('$app/forms', () => ({
enhance: () => ({ destroy: () => {} })
}));
describe('SignupForm', () => { describe('SignupForm', () => {
it('renders all form fields with correct labels', () => { it('renders all form fields with correct labels', () => {
render(SignupForm); render(SignupForm);
@@ -131,6 +135,34 @@ describe('SignupForm', () => {
expect(screen.getByLabelText('Passwort')).toHaveAttribute('autocomplete', 'new-password'); expect(screen.getByLabelText('Passwort')).toHaveAttribute('autocomplete', 'new-password');
}); });
it('displays server-side form error when form prop has errors', () => {
render(SignupForm, {
props: {
form: {
errors: { form: 'Registrierung fehlgeschlagen. Bitte versuche es erneut.' },
displayName: 'Sarah',
email: 'sarah@example.com'
}
}
});
expect(
screen.getByText('Registrierung fehlgeschlagen. Bitte versuche es erneut.')
).toBeInTheDocument();
});
it('displays server-side field errors from form prop', () => {
render(SignupForm, {
props: {
form: {
errors: { email: 'Ungültige E-Mail-Adresse' },
displayName: 'Sarah',
email: 'bad'
}
}
});
expect(screen.getByText('Ungültige E-Mail-Adresse')).toBeInTheDocument();
});
it('renders placeholders on inputs', () => { it('renders placeholders on inputs', () => {
render(SignupForm); render(SignupForm);
expect(screen.getByPlaceholderText('z.B. Sarah')).toBeInTheDocument(); expect(screen.getByPlaceholderText('z.B. Sarah')).toBeInTheDocument();

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import BrandPanel from '$lib/auth/BrandPanel.svelte'; import BrandPanel from '$lib/auth/BrandPanel.svelte';
import SignupForm from '$lib/auth/SignupForm.svelte'; import SignupForm from '$lib/auth/SignupForm.svelte';
let { form } = $props();
</script> </script>
<svelte:head> <svelte:head>
@@ -12,7 +14,7 @@
<BrandPanel /> <BrandPanel />
<div class="flex flex-1 flex-col items-start justify-center px-[20px] py-[24px] md:items-center md:px-[56px] md:py-[48px]"> <div class="flex flex-1 flex-col items-start justify-center px-[20px] py-[24px] md:items-center md:px-[56px] md:py-[48px]">
<div class="w-full max-w-[380px]"> <div class="w-full max-w-[380px]">
<SignupForm /> <SignupForm {form} />
</div> </div>
</div> </div>
</div> </div>