Scaffold a form that posts through SvelteKit remote functions instead of classic form actions. The generator emits a +page.svelte, a form.remote.ts handler, a Zod schema, and a server test from one field list. Standard field types — text, number, email, editor, select, bool, file — render with matching shadcn-svelte inputs, and file fields switch the form to multipart/form-data automatically. Also patches svelte.config.js to enable the experimental remote-functions and async compiler options if they are not already enabled.
<script lang="ts">
import { submitContactForm } from "./form.remote";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Textarea } from "$lib/components/ui/textarea";
</script>
<section data-role="content">
<div class="flex justify-between items-center mb-4">
<h1 class="text-3xl font-bold tracking-tight">Contact</h1>
</div>
<div class="bg-card rounded-lg shadow-sm border p-4">
<form {...submitContactForm}>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2 col-span-1">
<label for="name" class="text-sm font-medium">Name</label>
<Input id="name" {...submitContactForm.fields.name.as("text")} />
{#each submitContactForm.fields.name.issues() as issue}
<p class="text-destructive text-sm">{issue.message}</p>
{/each}
</div>
<div class="space-y-2 col-span-1">
<label for="email" class="text-sm font-medium">Email</label>
<Input
id="email"
{...submitContactForm.fields.email.as("text")}
type="email"
/>
{#each submitContactForm.fields.email.issues() as issue}
<p class="text-destructive text-sm">{issue.message}</p>
{/each}
</div>
<div class="space-y-2 col-span-2">
<label for="message" class="text-sm font-medium">Message</label>
<Textarea
id="message"
{...submitContactForm.fields.message.as("text")}
/>
{#each submitContactForm.fields.message.issues() as issue}
<p class="text-destructive text-sm">{issue.message}</p>
{/each}
</div>
</div>
<div class="mt-4 flex gap-2 border-t pt-4 -mx-4 px-4">
<Button type="submit">Submit</Button>
</div>
</form>
</div>
</section>
import { form, getRequestEvent } from "$app/server";
import { setFlash } from "sveltekit-flash-message/server";
import { contactSchema } from "$lib/schemas/contact";
export const submitContactForm = form(contactSchema, async (data) => {
const { cookies } = getRequestEvent();
setFlash({ type: "toast", message: "Form posted successfully" }, cookies);
return { success: true };
});
import { afterEach, beforeEach, describe, expect, it } from "vitest";
describe("/contact", () => {
describe("GET /contact", () => {
it("should return a 200 status code", async (context) => {
const response = await context.request.get("/contact");
expect(response.status).toBe(200);
});
});
});
import { z } from "zod";
export const contactSchema = z.object({
name: z.string().optional(),
email: z.email().optional(),
message: z.string().optional(),
});
import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter,
experimental: {
remoteFunctions: true,
},
},
compilerOptions: {
experimental: {
async: true,
},
},
};
export default config;