Custom Footer with Multiple Actions

Replace the default submit button using the footer snippet. The snippet receives isSubmitting, isValid, and handleSubmit. Use controller.reset() to reset all fields programmatically.


Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5	import { Button } from "@kareyes/aether";
6
7	const DraftSchema = pipe(
8		Schema.Struct({
9			title: pipe(Schema.String, Schema.minLength(3), withField({ label: "Article Title" })),
10			category: pipe(
11				Schema.String,
12				withField({
13					label: "Category",
14					inputType: "select",
15					options: [
16						{ value: "tech", label: "Technology" },
17						{ value: "design", label: "Design" }
18					]
19				})
20			),
21			content: pipe(
22				Schema.String,
23				Schema.minLength(10),
24				withField({ label: "Content", inputType: "textarea" })
25			)
26		}),
27		withFormLayout({ columns: 1, sections: [{ id: "main", title: "New Article" }] })
28	);
29
30	const controller = new FormController(DraftSchema, { validateOnBlur: true });
31
32	let saveStatus = $state(null);
33
34	function handleSaveDraft() {
35		saveStatus = "Draft saved!";
36		setTimeout(() => (saveStatus = null), 2000);
37	}
38</script>
39
40<!-- The footer snippet receives isSubmitting, isValid, and handleSubmit -->
41<SchemaForm {controller} onSubmit={(d) => console.log("Published:", d)}>
42	{#snippet footer({ isSubmitting, isValid, handleSubmit })}
43		<div class="flex items-center justify-between gap-3 pt-2">
44			<div class="flex gap-2">
45				<Button variant="outline" size="sm" onclick={() => controller.reset()}>
46					Discard
47				</Button>
48				<Button variant="outline" size="sm" onclick={handleSaveDraft}>
49					{saveStatus ?? "Save Draft"}
50				</Button>
51			</div>
52			<Button size="sm" onclick={handleSubmit} disabled={isSubmitting || !isValid}>
53				{isSubmitting ? "Publishing..." : "Publish Article"}
54			</Button>
55		</div>
56	{/snippet}
57</SchemaForm>

4-Step Job Application

A real-world multi-step form with 4 steps and 13 fields across personal info, position details, experience, and agreement sections. Each step validates independently before allowing progression.

Personal
Your contact details
Position
Role and compensation
Experience
Background and cover letter
Submit
Review and agree

Personal

Your contact details

Personal Info


Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import {
5		SchemaForm, FormController, withField, withFormLayout,
6		RequiredCheckbox, requiredCheckbox
7	} from "@kareyes/aether/forms";
8
9	// A 4-step multi-step form.
10	// Each field is tagged with step: N and section: "id".
11	// The form validates each step independently before allowing progression.
12	const JobApplicationSchema = pipe(
13		Schema.Struct({
14			// Step 1 — Personal Info
15			firstName: pipe(
16				Schema.String, Schema.minLength(1),
17				withField({ label: "First Name", step: 1, section: "personal", colSpan: 6 })
18			),
19			lastName: pipe(
20				Schema.String, Schema.minLength(1),
21				withField({ label: "Last Name", step: 1, section: "personal", colSpan: 6 })
22			),
23			email: pipe(
24				Schema.String,
25				Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
26				Schema.annotations({ message: () => "Enter a valid email" }),
27				withField({ label: "Email", inputType: "email", step: 1, section: "personal" })
28			),
29
30			// Step 2 — Position
31			position: pipe(
32				Schema.String,
33				withField({
34					label: "Position Applied For",
35					inputType: "select",
36					step: 2,
37					section: "position",
38					options: [
39						{ value: "frontend", label: "Frontend Developer" },
40						{ value: "backend", label: "Backend Developer" },
41						{ value: "pm", label: "Product Manager" }
42					]
43				})
44			),
45			salaryExpectation: pipe(
46				Schema.Number,
47				withField({ label: "Salary Expectation (USD)", inputType: "number", step: 2, section: "position" })
48			),
49
50			// Step 3 — Experience
51			coverLetter: pipe(
52				Schema.String,
53				Schema.minLength(50),
54				Schema.annotations({ message: () => "Cover letter must be at least 50 characters" }),
55				withField({ label: "Cover Letter", inputType: "textarea", step: 3, section: "experience" })
56			),
57
58			// Step 4 — Agreements
59			backgroundCheck: pipe(
60				RequiredCheckbox,
61				withField({ label: "I consent to a background check", inputType: "checkbox", step: 4, section: "agreements" })
62			),
63			termsAgreement: pipe(
64				requiredCheckbox("You must agree to the application terms"),
65				withField({ label: "I agree to the terms and conditions", inputType: "checkbox", step: 4, section: "agreements" })
66			)
67		}),
68		withFormLayout({
69			columns: 12,
70			sections: [
71				{ id: "personal", title: "Personal Info" },
72				{ id: "position", title: "Position" },
73				{ id: "experience", title: "Experience" },
74				{ id: "agreements", title: "Agreements" }
75			],
76			steps: [
77				{ step: 1, title: "Personal", description: "Your contact details" },
78				{ step: 2, title: "Position", description: "Role and compensation" },
79				{ step: 3, title: "Experience", description: "Background and cover letter" },
80				{ step: 4, title: "Submit", description: "Review and agree" }
81			]
82		})
83	);
84
85	const controller = new FormController(JobApplicationSchema, {
86		validateOnBlur: true,
87		initialValues: { salaryExpectation: 0 }
88	});
89</script>
90
91<SchemaForm
92	{controller}
93	onSubmit={(d) => console.log("Application submitted:", d)}
94	showStepIndicator={true}
95	nextText="Next Step"
96	prevText="Previous"
97	submitText="Submit Application"
98/>

App Settings — Card Sections + Custom Footer

A settings panel combining sectionVariant="card" for visual grouping with a custom footer snippet that shows a "Saved!" confirmation. Demonstrates how layout props and footer customization compose together.

General Settings

Temporarily block access to non-admin users

Email Configuration
Security

Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5	import { Button } from "@kareyes/aether";
6
7	const AppSettingsSchema = pipe(
8		Schema.Struct({
9			siteName: pipe(
10				Schema.String, Schema.minLength(1),
11				Schema.annotations({ message: () => "Site name is required" }),
12				withField({ label: "Site Name", placeholder: "My Awesome App", section: "general" })
13			),
14			siteUrl: pipe(
15				Schema.String,
16				Schema.pattern(/^https?:\/\/.+/),
17				Schema.annotations({ message: () => "URL must start with http:// or https://" }),
18				withField({ label: "Site URL", inputType: "url", section: "general" })
19			),
20			maintenanceMode: pipe(
21				Schema.Boolean,
22				withField({ label: "Enable maintenance mode", inputType: "switch", section: "general" })
23			),
24			emailProvider: pipe(
25				Schema.String,
26				withField({
27					label: "Email Provider",
28					inputType: "select",
29					section: "email",
30					options: [
31						{ value: "smtp", label: "SMTP" },
32						{ value: "sendgrid", label: "SendGrid" },
33						{ value: "ses", label: "Amazon SES" }
34					]
35				})
36			),
37			maxLoginAttempts: pipe(
38				Schema.Number,
39				withField({ label: "Max Login Attempts", inputType: "number", section: "security", colSpan: 6 })
40			),
41			twoFactorRequired: pipe(
42				Schema.Boolean,
43				withField({ label: "Require 2FA", inputType: "switch", section: "security" })
44			)
45		}),
46		withFormLayout({
47			columns: 12,
48			sections: [
49				{ id: "general", title: "General Settings", order: 1 },
50				{ id: "email", title: "Email Configuration", order: 2 },
51				{ id: "security", title: "Security", order: 3 }
52			]
53		})
54	);
55
56	const controller = new FormController(AppSettingsSchema, {
57		validateOnBlur: true,
58		initialValues: { maintenanceMode: false, maxLoginAttempts: 5, twoFactorRequired: false }
59	});
60
61	let settingsSaved = $state(false);
62</script>
63
64<!-- Combining sectionVariant="card" with a custom footer snippet -->
65<SchemaForm
66	{controller}
67	onSubmit={() => { settingsSaved = true; setTimeout(() => settingsSaved = false, 2000); }}
68	sectionVariant="card"
69>
70	{#snippet footer({ isSubmitting, isValid, handleSubmit })}
71		<div class="flex items-center justify-between gap-4">
72			<Button variant="outline" size="sm" onclick={() => controller.reset()}>
73				Reset to Defaults
74			</Button>
75			<div class="flex items-center gap-3">
76				{#if settingsSaved}
77					<span class="text-sm text-green-600 font-medium">Settings saved!</span>
78				{/if}
79				<Button size="sm" onclick={handleSubmit} disabled={isSubmitting || !isValid}>
80					{isSubmitting ? "Saving..." : "Save Settings"}
81				</Button>
82			</div>
83		</div>
84	{/snippet}
85</SchemaForm>

E-commerce Checkout Flow

A 3-step checkout form covering contact, shipping (with method selection and address validation), and payment fields. Features mixed input types including radio groups, switches, and required checkboxes for terms agreement.

Contact
Your email and phone
Shipping
Delivery address and method
Payment
Card and billing info

Contact

Your email and phone

Contact


Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import {
5		SchemaForm, FormController, withField, withFormLayout,
6		RequiredCheckbox
7	} from "@kareyes/aether/forms";
8
9	// A 3-step checkout with contact, shipping, and payment sections.
10	const CheckoutSchema = pipe(
11		Schema.Struct({
12			// Step 1 — Contact
13			email: pipe(
14				Schema.String,
15				Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
16				Schema.annotations({ message: () => "Enter a valid email" }),
17				withField({ label: "Email Address", inputType: "email", step: 1, section: "contact" })
18			),
19			createAccount: pipe(
20				Schema.Boolean,
21				withField({
22					label: "Create an account for faster checkout next time",
23					inputType: "checkbox",
24					step: 1,
25					section: "contact"
26				})
27			),
28
29			// Step 2 — Shipping
30			shippingFirst: pipe(
31				Schema.String, Schema.minLength(1),
32				withField({ label: "First Name", step: 2, section: "shipping", colSpan: 6 })
33			),
34			shippingLast: pipe(
35				Schema.String, Schema.minLength(1),
36				withField({ label: "Last Name", step: 2, section: "shipping", colSpan: 6 })
37			),
38			shippingAddress: pipe(
39				Schema.String, Schema.minLength(5),
40				withField({ label: "Street Address", step: 2, section: "shipping" })
41			),
42			shippingMethod: pipe(
43				Schema.String,
44				withField({
45					label: "Shipping Method",
46					inputType: "radio",
47					step: 2,
48					section: "shipping",
49					options: [
50						{ value: "standard", label: "Standard (5-7 days) — Free" },
51						{ value: "express", label: "Express (2-3 days) — $9.99" },
52						{ value: "overnight", label: "Overnight — $24.99" }
53					]
54				})
55			),
56
57			// Step 3 — Payment
58			cardNumber: pipe(
59				Schema.String,
60				withField({ label: "Card Number", placeholder: "1234 5678 9012 3456", step: 3, section: "payment" })
61			),
62			cardCvv: pipe(
63				Schema.String,
64				Schema.pattern(/^\d{3,4}$/),
65				Schema.annotations({ message: () => "CVV must be 3 or 4 digits" }),
66				withField({ label: "CVV", inputType: "password", step: 3, section: "payment", colSpan: 4 })
67			),
68			orderTerms: pipe(
69				RequiredCheckbox,
70				withField({ label: "I agree to the terms of sale", inputType: "checkbox", step: 3, section: "payment" })
71			)
72		}),
73		withFormLayout({
74			columns: 12,
75			sections: [
76				{ id: "contact", title: "Contact" },
77				{ id: "shipping", title: "Shipping" },
78				{ id: "payment", title: "Payment" }
79			],
80			steps: [
81				{ step: 1, title: "Contact", description: "Your email and phone" },
82				{ step: 2, title: "Shipping", description: "Delivery address and method" },
83				{ step: 3, title: "Payment", description: "Card and billing info" }
84			]
85		})
86	);
87
88	const controller = new FormController(CheckoutSchema, {
89		validateOnBlur: true,
90		initialValues: { createAccount: false }
91	});
92</script>
93
94<SchemaForm
95	{controller}
96	onSubmit={(d) => console.log("Order placed:", d)}
97	showStepIndicator={true}
98	nextText="Continue"
99	prevText="Back"
100	submitText="Place Order"
101/>