Form
A composable form layout with sections, collapsible areas, flip-card read/edit panels, sub-sections, and built-in Goshtoso field rendering. Supports HTMX validation and external submit triggers.
// Built-in field types — set Input, Select, Combobox, Textarea, etc. directly.
// FieldGroup.ID remains the wrapper/HTMX target; omit the nested ID to let
// FieldGroup derive a unique focus target.
@form.FieldGroup(form.FieldGroupConfig{
ID: "name", Label: "Name", Required: true,
Input: &textinput.Config{Name: "name"},
})
@form.FieldGroup(form.FieldGroupConfig{
ID: "provider", Label: "Provider",
Select: &selectfield.Config{Name: "provider", Options: providerOptions},
})
// Link a summary to the actual focus target without guessing suffixes.
field := form.FieldGroupConfig{ID: "provider", Select: &selectfield.Config{Name: "provider"}}
item := form.FormErrorItem{Message: "Choose a provider", TargetID: field.FocusTargetID()}
// Custom component — use { children... } only when no built-in matches and
// wire its label, required state, errors, and hints explicitly.
// Flip Section — read-only front, editable back
@form.FlipSection(form.FlipSectionConfig{
SectionConfig: form.SectionConfig{Title: "Network"},
}, networkReadView()) {
@form.FieldGroup(form.FieldGroupConfig{
Label: "Pod CIDR",
Input: &textinput.Config{ID: "pod-cidr", Name: "pod_cidr", Value: "10.244.0.0/16"},
})
}
// External submit (modal pattern — no footer, modal confirmation calls requestSubmit)
@form.Form(form.Config{
ID: "modal-form",
HTMX: &form.HTMXConfig{
Post: "/api/submit",
Swap: "none",
},
}) { ... }
@modal.Modal(modal.Config{
ID: "modalSubmitConfirm",
Title: "Confirm submit",
TriggerLabel: "Submit",
PrimaryLabel: "Confirm submit",
SecondaryLabel: "Cancel",
PrimaryAction: &modal.ButtonAction{
OnClick: "document.getElementById('modal-form').requestSubmit()",
},
})
@toast.ToastContainer(toast.ContainerConfig{})Field Validation States
FieldGroup surfaces success/error input states, error messages (Errors), and helper hints (Hints).
Validation Examples
@form.FieldGroup(form.FieldGroupConfig{
ID: "error-field", Label: "Error Field", Required: true,
Errors: []string{"This cluster ID is already taken"},
Input: &textinput.Config{ID: "error-field", Name: "error", State: textinput.StateError},
})
@form.FieldGroup(form.FieldGroupConfig{
ID: "hint-field", Label: "Field with Hints",
Hints: []string{"Must be 3-63 characters", "Lowercase letters, numbers, hyphens"},
Input: &textinput.Config{ID: "hint-field", Name: "hints"},
})Server-Side Validation
Server-side field validation with HTMX. Fields validate on change, dependent fields update automatically (e.g. slug from name), and full-form submit validates everything at once.
Try it out
Type in the fields below and tab out to see validation in action. The slug field auto-generates from name. Try "admin", "test", or "demo" as slugs to see the uniqueness check.
// 1. Define the form fields and validation rules
def := &validation.FormDef{
FormID: "demo-validation",
Endpoint: "/api/components/form-validation",
Fields: map[string]*validation.FieldDef{
"name": {Name: "name", FieldGroup: nameField, OnChange: true},
"slug": {Name: "slug", FieldGroup: slugField, OnChange: true, DependsOn: []string{"name"}},
"email": {Name: "email", FieldGroup: emailField, OnChange: true},
},
}
def.Bind() // wires up HTMX attributes and metadata
// 2. In the templ, render the form with FieldGroups
@form.Form(form.Config{
ID: "demo-validation",
HTMX: &form.HTMXConfig{Post: "/api/components/form-validation", Target: "#form-result", Swap: "innerHTML"},
Footer: &form.FooterConfig{SubmitLabel: "Submit", CancelLabel: "Reset"},
}) {
@form.Section(form.SectionConfig{Title: "Project Details"}) {
@form.FieldGroup(*nameField)
@form.FieldGroup(*slugField)
@form.FieldGroup(*emailField)
}
}
// 3. Handle validation in Go
func (s *Server) handleFormValidation(w http.ResponseWriter, r *http.Request) {
def := buildDemoFormDef()
result := validation.Handle(r, def, validateDemoField)
if validation.IsFieldValidation(r) {
validation.RenderFieldResponse(r.Context(), w, *result)
return
}
// full submit...
}
// 4. Per-field validation hook
func validateDemoField(ctx validation.ValidationContext, name string, fg *form.FieldGroupConfig) bool {
switch name {
case "name":
val := ctx.FormValues["name"]
if len(val) < 3 {
fg.Errors = []string{"Name must be at least 3 characters."}
fg.Input.State = textinput.StateError
return false
}
fg.Input.State = textinput.StateSuccess
return true
case "slug":
// auto-generate from name on dependency trigger
if ctx.Type == validation.ValidationDependency && val == "" { ... }
// uniqueness check against takenSlugs map
case "email":
// format validation
}
}Responsive Sticky Footer
Sticky form actions stack at narrow widths, keep long labels inside the viewport, use an opaque safe-area-aware surface, and retain their normal-flow footprint so the last control remains reachable.
External Submit (Modal Pattern)
Render the form with no Footer; a button outside the form uses the form="..." attribute to submit it — the typical modal pattern.
Confirm upgrade
Submit the upgrade request with the selected target version.
@form.Form(form.Config{
ID: "external-form",
HTMX: &form.HTMXConfig{
Post: "/api/components/form/external-submit",
Swap: "none",
},
}) {
@form.Section(form.SectionConfig{Title: "Upgrade"}) { ... }
}
@modal.Modal(modal.Config{
ID: "externalSubmitConfirm",
Title: "Confirm upgrade",
TriggerLabel: "Upgrade (External Button)",
PrimaryLabel: "Confirm upgrade",
SecondaryLabel: "Cancel",
PrimaryAction: &modal.ButtonAction{
OnClick: "document.getElementById('external-form').requestSubmit()",
},
})
@toast.ToastContainer(toast.ContainerConfig{})Go API
v0.2.7The examples above cover behavior and composition. pkg.go.dev is the canonical reference for exported types, functions, methods, and Go documentation.
github.com/araihu/goshtoso/components/form