Skip to content

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.

Default Rendered example

Identification

Lowercase letters, numbers, and hyphens only
TagsList is a built-in FieldGroup control

Network

Pod Subnet CIDR 10.244.0.0/16
Service Subnet CIDR 10.96.0.0/12
Default: 10.244.0.0/16
Default: 10.96.0.0/12
  • Info
  • Debug
  • Warning
  • Error

Node Pools

Control Plane

  • m5.large (2 vCPU, 8 GB)
  • m5.xlarge (4 vCPU, 16 GB)

Workers

  • m5.xlarge (4 vCPU, 16 GB)
  • m5.2xlarge (8 vCPU, 32 GB)
Cancel
Usage Example
// 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).

Field Validation States Rendered example

Validation Examples

This cluster ID is already taken
Must be between 3 and 63 characters Only lowercase letters, numbers, and hyphens
Field Validation States
@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.

Server-Side Validation Rendered example

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.

Project Details

At least 3 characters.
URL-friendly identifier. Auto-generated from name.
Contact email for notifications.
Reset
Server-Side Validation
// 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
    }
}

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.

Responsive Sticky Footer Rendered example
Responsive Sticky Footer

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.

External Submit (Modal Pattern) Rendered example

Upgrade

  • v1.32.0
  • v1.31.5
  • v1.30.8
External Submit (Modal Pattern)
@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.7

The 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
Open v0.2.7 API