Contents

Tier 1 controls map directly onto native HTML form elements. They live in github.com/zatrano/goui/forms and share two embedded helper types:

import "github.com/zatrano/goui/forms"
  • forms.CommonAttrs — the attributes almost every control accepts: ID, Class, Title, TabIndex *int, Spellcheck *bool, Draggable *bool, AriaLabel, AriaDescribedBy, Autocomplete, Disabled, ReadOnly, Required, Autofocus, Name.
  • forms.FieldValidationRules []validation.Rule and Errors []string; see 06-validation.md for the full validation story. Embedding it gives you Validate() bool on the field.

Every Tier 1 field also implements the shared forms.FieldValue contract (Name() string, RawValue() string, SetRawValue(string)), which is what forms.ValidateAll and generic form-binding code rely on.

All examples below assume:

import (
    "github.com/zatrano/goui/core"
    "github.com/zatrano/goui/forms"
)

TextInput

Covers type=text|password|email|search|tel|url.

type TextInput struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Type          string
    Value         string
    Placeholder   string
    MinLength     int
    MaxLength     int
    Pattern       string
    Size          int
    Multiple      bool // email
    List          string
    EventName     string // g-change / g-input event name (defaults to Name)
    DebounceMS    int
    OnChange      func(newValue string)
    ShowCharCount bool
    ShowStrength  bool   // password strength meter (server-side)
    HelperText    string // hint below the field
}
email := forms.TextInput{
    CommonAttrs: forms.CommonAttrs{Name: "email", ID: "email", Required: true},
    Type:        "email",
    Placeholder: "[email protected]",
}
html, err := email.Render()
<input id="email" name="email" required type="email"
       value="" placeholder="[email protected]"
       g-change="email" g-input="email">

ShowCharCount/ShowStrength render extra <p>/<div> metadata below the input (see forms.fieldMetaHTML); ShowStrength only takes effect when Type == "password".


NumericInput

Covers type=number|range.

type NumericInput struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Type      string
    Value     string
    Min       string
    Max       string
    Step      string
    EventName string
    OnChange  func(newValue string)
}
qty := forms.NumericInput{
    CommonAttrs: forms.CommonAttrs{Name: "qty", ID: "qty"},
    Type:        "number",
    Min:         "0",
    Max:         "10",
    Step:        "1",
}
<input id="qty" name="qty" type="number" value="" min="0" max="10" step="1"
       g-change="qty" g-input="qty">

DateTimeInput

Covers type=date|time|datetime-local|month|week.

type DateTimeInput struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Type      string
    Value     string
    Min       string
    Max       string
    Step      string
    EventName string
    OnChange  func(newValue string)
}
birthday := forms.DateTimeInput{
    CommonAttrs: forms.CommonAttrs{Name: "birthday", ID: "birthday"},
    Type:        "date",
}
<input id="birthday" name="birthday" type="date" value="" g-change="birthday">

ChoiceInput (checkbox / radio)

Covers type=checkbox|radio. CheckboxInput and RadioInput are type aliases of the same struct — purely for readability at the call site.

type ChoiceInput struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Type      string // checkbox | radio
    Value     string // submitted value when checked
    Checked   bool
    EventName string
    LabelText string // optional adjacent label text
    OnChange  func(checked bool, value string)
}

type CheckboxInput = ChoiceInput
type RadioInput = ChoiceInput
subscribe := forms.ChoiceInput{
    CommonAttrs: forms.CommonAttrs{Name: "subscribe", ID: "subscribe"},
    Type:        "checkbox",
    Value:       "yes",
    LabelText:   "Subscribe to newsletter",
}
<input id="subscribe" name="subscribe" type="checkbox" value="yes" g-change="subscribe">
<span>Subscribe to newsletter</span>

FileInput

Covers type=file. Only tracks the last selected file name(s) client-side; actual binary upload is a separate flow (see forms.DragDropUpload / forms.AvatarUpload in 07-forms-tier2.md and the upload package).

type FileInput struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Accept    string
    Capture   string
    Multiple  bool
    EventName string
    OnChange  func(fileNames string)
    Value     string // last selected file name(s) for display/state
}
avatar := forms.FileInput{
    CommonAttrs: forms.CommonAttrs{Name: "avatar", ID: "avatar"},
    Accept:      "image/*",
}
<input id="avatar" name="avatar" type="file" accept="image/*" g-change="avatar">

ColorInput

Covers type=color. Not validated (no FieldValidation embed); defaults to #000000 when Value is empty.

type ColorInput struct {
    core.BaseComponent
    CommonAttrs
    Value     string
    EventName string
    OnChange  func(newValue string)
}
brand := forms.ColorInput{
    CommonAttrs: forms.CommonAttrs{Name: "brand", ID: "brand"},
    Value:       "#2563eb",
}
<input id="brand" name="brand" type="color" value="#2563eb" g-change="brand">

HiddenInput

Covers type=hidden. No validation, no events (HandleEvent is a no-op — hidden fields are set programmatically from server logic, not user input).

type HiddenInput struct {
    core.BaseComponent
    CommonAttrs
    Value string
}
csrf := forms.HiddenInput{
    CommonAttrs: forms.CommonAttrs{Name: "csrf_token"},
    Value:       "abc123",
}
<input name="csrf_token" type="hidden" value="abc123">

Textarea

type Textarea struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Value         string
    Placeholder   string
    Rows          int
    Cols          int
    Wrap          string
    MinLength     int
    MaxLength     int
    EventName     string
    DebounceMS    int
    OnChange      func(newValue string)
    ShowCharCount bool
    HelperText    string
}
message := forms.Textarea{
    CommonAttrs:   forms.CommonAttrs{Name: "message", ID: "message"},
    Rows:          4,
    MaxLength:     500,
    ShowCharCount: true,
}
<textarea id="message" name="message" rows="4" maxlength="500"
          g-change="message" g-input="message" g-debounce="100"></textarea>
<p class="goui-char-count text-sm">0 / 500</p>

Select, Option, Optgroup

type Option struct {
    Value    string
    Label    string
    Selected bool
    Disabled bool
}

type Optgroup struct {
    Label    string
    Disabled bool
    Options  []Option
}

type Select struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Value     string
    Multiple  bool
    Size      int
    Options   []Option
    Groups    []Optgroup
    EventName string
    OnChange  func(newValue string)
}
country := forms.Select{
    CommonAttrs: forms.CommonAttrs{Name: "country", ID: "country"},
    Options: []forms.Option{
        {Value: "", Label: "Select a country"},
        {Value: "tr", Label: "Türkiye"},
        {Value: "us", Label: "United States"},
    },
}
<select id="country" name="country" g-change="country">
  <option value="">Select a country</option>
  <option value="tr">Türkiye</option>
  <option value="us">United States</option>
</select>

Optgroup renders as a nested <optgroup label="..."> block after the top-level Options. HandleEvent re-derives every Option.Selected (in both Options and every Optgroup.Options) from the incoming value, so the struct stays consistent for subsequent renders.


Button

Covers type=submit|button|reset|image.

type Button struct {
    core.BaseComponent
    CommonAttrs
    Type      string
    Value     string
    Text      string
    Src       string // type=image
    Alt       string
    EventName string // g-click event
}
save := forms.Button{Type: "button", Text: "Save", EventName: "save"}
<button type="button" class="goui-button ..." g-click="save">Save</button>

Type == "image" renders an <input type="image" src="..." alt="..."> instead of a <button> element.


Form, Fieldset, Legend, Label

Structural containers with no server-side state of their own — they render attributes plus caller-supplied inner HTML (composed with forms.JoinHTML).

type Form struct {
    core.BaseComponent
    CommonAttrs
    Action    string
    Method    string
    EncType   string
    InnerHTML string
    OnSubmit  string // g-submit event name
}

type Fieldset struct {
    core.BaseComponent
    CommonAttrs
    InnerHTML string
}

type Legend struct {
    core.BaseComponent
    CommonAttrs
    Text string
}

type Label struct {
    core.BaseComponent
    CommonAttrs
    For  string
    Text string
}
nameLabel, _ := (&forms.Label{For: "name", Text: "Name"}).Render()
nameInput, _ := (&forms.TextInput{CommonAttrs: forms.CommonAttrs{Name: "name", ID: "name"}}).Render()

form := &forms.Form{
    Method:   "post",
    OnSubmit: "save",
    InnerHTML: forms.JoinHTML(
        `<div class="field">`, nameLabel, nameInput, `</div>`,
    ),
}
html, err := form.Render()
<form class="goui-form ..." method="post" g-submit="save">
  <div class="field">
    <label class="goui-label ..." for="name">Name</label>
    <input id="name" name="name" type="text" value="">
  </div>
</form>

g-submit intercepts the native form submission (preventDefault) and sends an event frame whose payload is {"fields": {...}} gathered from every named form control via FormData — see 06-validation.md for a complete form example wiring this up end-to-end.


Datalist

Backs an input's list="<id>" attribute for native autocomplete suggestions.

type DatalistOption struct {
    Value string
    Label string
}

type Datalist struct {
    core.BaseComponent
    CommonAttrs
    Options []DatalistOption
}
cities := forms.Datalist{
    CommonAttrs: forms.CommonAttrs{ID: "cities"},
    Options: []forms.DatalistOption{
        {Value: "ist", Label: "İstanbul"},
        {Value: "ank", Label: "Ankara"},
    },
}
<datalist id="cities">
  <option value="ist" label="İstanbul">İstanbul</option>
  <option value="ank" label="Ankara">Ankara</option>
</datalist>
<input list="cities">

Output

Displays a calculation result (e.g. the result of a <form> with linked <input>s via for).

type Output struct {
    core.BaseComponent
    CommonAttrs
    For   string
    Form  string
    Value string
    Text  string
}
summary := forms.Output{
    CommonAttrs: forms.CommonAttrs{Name: "summary"},
    Text:        "Name: Ada | Email: [email protected]",
}
<output name="summary">Name: Ada | Email: [email protected]</output>

Meter

Represents a scalar measurement within a known range.

type Meter struct {
    core.BaseComponent
    CommonAttrs
    Value   float64
    Min     float64
    Max     float64
    Low     float64
    High    float64
    Optimum float64
}
disk := forms.Meter{Value: 0.6, Min: 0, Max: 1, High: 0.9, Optimum: 0.3}
<meter value="0.6" min="0" max="1" high="0.9" optimum="0.3"></meter>

Progress

Represents task completion. Max defaults to 1 when left at the zero value.

type Progress struct {
    core.BaseComponent
    CommonAttrs
    Value float64
    Max   float64
}
upload := forms.Progress{Value: 0.42}
<progress value="0.42" max="1"></progress>

Putting it together

A minimal registered component composing several Tier 1 fields:

type ContactForm struct {
    core.BaseComponent
    Name  forms.TextInput
    Email forms.TextInput
}

func NewContactForm() *ContactForm {
    return &ContactForm{
        Name:  forms.TextInput{CommonAttrs: forms.CommonAttrs{Name: "name", ID: "name"}},
        Email: forms.TextInput{CommonAttrs: forms.CommonAttrs{Name: "email", ID: "email"}, Type: "email"},
    }
}

func (c *ContactForm) Mount(_ context.Context) error   { return nil }
func (c *ContactForm) Unmount(_ context.Context) error { return nil }

func (c *ContactForm) HandleEvent(ctx context.Context, event string, payload map[string]any) error {
    switch event {
    case "name":
        return c.Name.HandleEvent(ctx, event, payload)
    case "email":
        return c.Email.HandleEvent(ctx, event, payload)
    }
    return nil
}

func (c *ContactForm) Render() (string, error) {
    nameHTML, _ := c.Name.Render()
    emailHTML, _ := c.Email.Render()
    return forms.JoinHTML(`<div class="field">`, nameHTML, `</div>`,
        `<div class="field">`, emailHTML, `</div>`), nil
}

Every Tier 1 field's HandleEvent responds to "change"/"input" (where applicable) and its own configured event name (EventName, defaulting to Name) — so dispatching by event string directly to the matching field's HandleEvent, as above, works without any extra plumbing. See the shipped examples/contact-form for a complete, runnable version with validation and toasts.