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.
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.
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.
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.
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/>