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, Combobox, Textarea, Toggle, etc. directly
@form.FieldGroup(form.FieldGroupConfig{
ID: "name", Label: "Name", Required: true,
Input: &textinput.Config{ID: "name", Name: "name"},
})
@form.FieldGroup(form.FieldGroupConfig{
ID: "provider", Label: "Provider",
Combobox: &gocombobox.Config{ID: "provider", Name: "provider", Source: gocombobox.Source{Static: opts}},
})
// Custom component — use { children... } for anything not built-in
@form.FieldGroup(form.FieldGroupConfig{ID: "tags", Label: "Tags"}) {
@myCustomTagsInput(tags) // any templ.Component
}
// 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.Container(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
}
}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.Container(toast.ContainerConfig{})API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
ID | string | "" | Form element id (target of external submit buttons via form="..."). |
Action | string | "" | Non-HTMX form action URL. |
Method | string | "post" | HTTP method for non-HTMX submits. |
HTMX | *HTMXConfig | nil | HTMX submit config (Post/Put/Target/Swap, inline validation). |
PreventEnterSubmit | *bool | nil | When true, Enter in a field does not submit the form. |
Footer | *FooterConfig | nil | Submit/cancel footer; omit for the external-submit pattern. |
RootClass | string | "" | Extra classes on the <form>. |
FieldGroup | FieldGroupConfig | — | Per-field wrapper: Label, Required, Errors, Hints + one built-in (Input/Combobox/Textarea/Toggle/...) or children. |
Section / SubSection | SectionConfig | — | Group fields under a titled section; SubSection nests within a Section. |
CollapsibleSection | CollapsibleSectionConfig | — | A Section that collapses, with a Summary shown when collapsed. |
FlipSection | FlipSectionConfig | — | A section with a read-only front view and an editable back. |
FormDef.FormID | string | "" | DOM id of the form (must match form.Config.ID). |
FormDef.Endpoint | string | "" | Server URL that handles field + full-form validation requests. |
FormDef.Fields | map[string]*FieldDef | nil | Per-field definitions keyed by field name. |
FieldDef.Name | string | "" | Form field name (matches the input's Name). |
FieldDef.FieldGroup | *form.FieldGroupConfig | nil | The field's FieldGroup (label, input, hints, errors). |
FieldDef.OnChange | bool | false | Validate this field on change (blur), not just on submit. |
FieldDef.DependsOn | []string | nil | Other field names that re-trigger this field's validation (e.g. slug depends on name). |
def.Bind() | method | — | Wires HTMX attributes + metadata onto each FieldGroup; call once after defining. |
validation.Handle | func | — | Server entry point: routes field vs full-form requests and runs your per-field validator. |