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.
- 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 ofstrconv.Atoi,fmt.Contains(s, sub)instead ofstrings.Contains. - No maps in inputs: Use slices. Maps increase WASM binary size.
- No
reflectat runtime.
// 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 stringform.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:
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 viaPermittedrules.
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 |
Fielder types can optionally implement Namer to customize the form name:
type Namer interface {
FormName() string
}If not implemented, defaults to "form".
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 |
|---|---|
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 |
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", ...).
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 | 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 |
- API Reference — additional detail on Validate, SyncValues, Permitted
- Design & Architecture — core layers and philosophy
- Struct Tags — tag format and runtime overrides
- Standard Types — quick reference of 17 input types
- Implementation Notes — file map, adding new inputs, constraints
- Interactivity & Mounting — WASM event handling detail