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.NewEmail()).
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:
- If
field.Widget == nil → field is skipped (no UI binding).
field.Widget.Clone(formID, fieldName) creates a positioned input instance.
- The result is type-asserted to
input.Input — if it fails, the field is skipped.
- Constraint-based defaults are applied (e.g.
NotNull → SetRequired(true)).
- 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.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:
NewXxx() fmt.Widget — 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
Creates a Form from a Fielder. Uses data.Schema() to resolve inputs via field.Widget. Form id = parentID + "." + resolveStructName(data).
Registers custom input types globally. Used for explicit lookup via findInputByType(). Not called automatically — no init() auto-registration.
Sets CSS classes applied to all new forms.
| 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.
17 built-in types in input/:
| Input |
HTML type |
Key Aliases |
Address |
text |
address, addr |
Checkbox |
checkbox |
check, boolean, bool |
Date |
date |
fecha |
Datalist |
text |
list, options |
Email |
email |
mail, correo |
Filepath |
text |
path, dir, file |
Gender |
radio |
gender, sex |
Hour |
time |
hour |
IP |
text |
ip |
Number |
number |
num, amount, price, age |
Password |
password |
pass, clave, pwd |
Phone |
tel |
phone, mobile, cell |
Radio |
radio |
-- |
Rut |
text |
rut, run, dni |
Select |
select |
-- |
Text |
text |
name, fullname, username |
Textarea |
textarea |
description, details, comments |
Tags are parsed at generation time by ormc and populate fmt.Field schema:
| Tag |
Format |
Description |
form |
"email" |
Force input type |
form |
"-" |
Exclude from form |
options |
"k1:v1,k2:v2" |
Select/radio/datalist options |
placeholder |
"text" |
Override auto-translated placeholder |
title |
"text" |
Override auto-translated title |
validate |
"false" |
Skip validation |
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