Skip to content

tinywasm/form

Repository files navigation

tinywasm/form

Fast form generation from Go structs for TinyGo + WASM. Uses fmt.Fielder interface for zero-reflection data binding. Input types are assigned via fmt.Widget in the schema (generated by ormc) — no magic name matching.

Rules (Non-Negotiable)

  • No stdlib imports in library code. Never errors, strconv, strings, reflect.
  • Use github.com/tinywasm/fmt: fmt.Err("Noun", "Adjective") for errors, fmt.Convert(s).Int() instead of strconv.Atoi, fmt.Contains(s, sub) instead of strings.Contains.
  • No maps in inputs: Use slices. Maps increase WASM binary size.
  • No reflect at runtime.

Quick Start

// User struct implements fmt.Fielder (generated by ormc).
// Each field in Schema() has a Widget assigned (e.g. input.Email()).
f, err := form.New("parent-id", &User{Name: "John"})
html := f.RenderHTML()    // render to HTML string

How Field Matching Works

form.New() iterates data.Schema(). For each field:

  1. If field.Widget == nil → field is skipped (no UI binding).
  2. field.Widget.Clone(formID, fieldName) creates a positioned input instance.
  3. The result is type-asserted to input.Input — if it fails, the field is skipped.
  4. Constraint-based defaults are applied (e.g. NotNullSetRequired(true)).
  5. Current value from data.Pointers() is bound to the input.

There is no registry-based name matching. Each field's Widget is set explicitly in the schema by ormc.

Input Interface

// input.Input — defined in input/interface.go
type Input interface {
    fmt.Widget    // Type(), Validate(value string) error, Clone(parentID, name string) Widget
    dom.Component // GetID(), SetID(), RenderHTML(), Children()
}

Each concrete type (Email, Text, etc.) provides:

  • Xxx() Input — template instance for use in schema (no position).
  • Clone(parentID, name string) fmt.Widget — creates a positioned copy ready for rendering.
  • Type() string — semantic input type name (e.g. "email").
  • Validate(value string) error — validates input value via Permitted rules.

Public API

form.New(parentID string, data fmt.Fielder) (*Form, error)

Creates a Form from a Fielder. Uses data.Schema() to resolve inputs via field.Widget. Form id = parentID + "." + resolveStructName(data).

form.RegisterInput(inputs ...input.Input)

Registers custom input types globally. Used for explicit lookup via findInputByType(). Not called automatically — no init() auto-registration.

form.SetGlobalClass(classes ...string)

Sets CSS classes applied to all new forms.

Form Methods

Method Description
GetID() string Returns form's HTML id
SetSSR(bool) *Form Enables SSR mode (adds method, action, submit button)
RenderHTML() string Generates form HTML
Validate() error Validates all inputs, returns first error
SyncValues(fmt.Fielder) error Copies input values back into the provided data
ValidateData(byte, fmt.Fielder) error Server-side validation (crudp.DataValidator)
OnSubmit(func(fmt.Fielder) error) *Form Sets WASM submit callback
OnMount() WASM only — sets up event delegation
Input(fieldName string) input.Input Returns input for a field name
SetOptions(fieldName, ...fmt.KeyValue) *Form Sets options for select/radio/datalist
SetValues(fieldName, ...string) *Form Sets value programmatically

Namer Interface

Fielder types can optionally implement Namer to customize the form name:

type Namer interface {
    FormName() string
}

If not implemented, defaults to "form".

WASM Event Flow

dom.Mount("root", f)
  -> f.RenderHTML() injected into DOM
  -> f.OnMount():
      el.On("input",  fn) -> SetValues + Validate per input
      el.On("change", fn) -> same (for select/radio/checkbox)
      el.On("submit", fn) -> PreventDefault -> SyncValues(f.data) -> Validate -> onSubmit(f.data)

One listener per form (not per input) = fewer closures = smaller WASM binary.

Standard Input Types

17 built-in types in input/:

Input HTML type
Address text
Checkbox checkbox
Date date
Datalist text
Email email
Filepath text
Gender radio
Hour time
IP text
Number number
Password password
Phone tel
Radio radio
Rut text
Select select
Text text
Textarea textarea

Struct Tags

The unified input: tag is parsed by ormc at generation time to populate the fmt.Field schema:

Feature Format Example
Widget input:"type" input:"email", input:"textarea", input:"-"
Validation input:"rule" input:"required", input:"min=5", input:"max=100"
Metadata input:"key=val" input:"title=\"Label\"", input:"placeholder=\"Hint\""
Options input:"options=..." input:"options=\"1:Yes,0:No\""

Multiple rules are comma-separated: `input:"email,required,max=100"`. See docs/TAGS.md for full reference.

Runtime overrides: f.Input("Field").SetPlaceholder(), f.SetOptions("Field", ...).

Validation Engine (fmt.Permitted)

type Permitted struct {
	Letters    bool     // a-z, A-Z (and ñ/Ñ)
	Tilde      bool     // á, é, í, ó, ú
	Numbers    bool     // 0-9
	Spaces     bool     // ' '
	BreakLine  bool     // '\n'
	Tab        bool     // '\t'
	Extra      []rune   // additional allowed characters
	NotAllowed []string // disallowed substrings
	Minimum    int      // minimum length
	Maximum    int      // maximum length
}

File Map

File Responsibility
form.go Form struct, New(), Input(), SetOptions(), SetValues(), Namer
sync.go SyncValues(), pointer-based field sync
registry.go RegisterInput(), findInputByType(), SetGlobalClass()
render.go RenderHTML(), SetSSR()
validate.go Validate()
validate_struct.go ValidateData() (crudp.DataValidator)
words.go Registers form UI words into fmt dictionary
mount.go OnMount(), OnUnmount() (wasm only)
input/interface.go Input interface (embeds fmt.Widget + dom.Component)
input/base.go Base struct embedded by all inputs
input/*.go 17 concrete input implementations

Documentation Index

About

Form generation using DOM and structs

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages