HTML forms generated from your model schema — validation included, no
reflection, TinyGo/WASM-ready. You don't declare forms: you declare a
model.Definition once, and fields whose
kind is an input.* type become form inputs automatically.
This package is part of the webtyp ecosystem — Go libraries for building full-stack web apps compiled to WebAssembly.
go get webtyp.com/form # brings webtyp/model as dependencyThe code generator tooling is covered in step 2 of the Quick Start.
Two imports: model provides the schema
types and base kinds (model.Text(), model.Int(), …); input is this
package's sub-package with the form kinds (input.Text(), input.Email(), …).
The kind you choose per field decides everything: input.* = form input +
validation; model.* = validation only (never rendered).
import (
"webtyp.com/model"
"webtyp.com/input"
)
var UserModel = model.Definition{
Name: "user",
Fields: model.Fields{
{Name: "id", Type: model.Int(), DB: &model.FieldDB{PK: true, AutoInc: true}},
{Name: "name", Type: input.Text(), NotNull: true},
{Name: "email", Type: input.Email(), NotNull: true},
{Name: "sku", Type: SKU()}, // custom input — defined in your own package
{Name: "notes", Type: model.Text()}, // validation only — never rendered
},
}Auto-increment PKs are skipped automatically (not editable).
A custom input like SKU() is a type in your own package that embeds
input.Base and configures its validation rules — it then works as a kind
like any built-in. Minimal shape (full pattern:
input/README.md):
type sku struct{ input.Base }
func SKU() input.Input {
s := &sku{}
s.Letters, s.Numbers = true, true
s.Maximum = 12
s.InitBase("", "", "text")
return s
}
func (s *sku) Clone(parentID, name string) input.Input {
c := *s
c.InitBase(parentID, name, "text")
return &c
}From your model.Definition vars, the generator emits <file>_orm.go next
to each model file: the row struct plus the model.Fielder methods
(Schema(), Pointers(), Values()). You never write these by hand.
Recommended: use the webtyp dev
environment — it watches your model files and regenerates *_orm.go
automatically with hot reload:
go install webtyp.com/app/cmd/webtyp@latest
webtyp -tui # interactive dev server (or -mcp for AI agents)Manual alternative: run ormc
(the standalone generator that webtyp uses internally) once in your
module:
go install webtyp.com/ormc/cmd/ormc@latest
ormcNote you pass the generated struct instance, not the Definition: the form
needs a data holder — it reads initial values from it and writes submitted
values back into it. The schema travels along anyway: the generated
Schema() method returns UserModel.Fields.
import "webtyp.com/form"
f, err := form.New("parent-id", &User{Name: "John"}, ids) // ids: model.IDGenerator (e.g. unixid.NewUnixID())
html := f.String() // SSR: render to HTML stringdom mounts components in the browser
(compiled with TinyGo):
import "webtyp.com/dom"
f.LoadValues(record) // registro → formulario (el usuario selecciona)
f.Validate() // el usuario edita y guarda
f.SyncValues(record) // formulario → registro (listo para enviar)
f.OnSubmit(func(data model.Fielder, done func(error)) {
// send data to your API, then:
done(nil) // nil = success → form resets (see NoResetOnSuccess)
})
dom.Mount("root", f)Mounted forms get live per-field validation on input, a submit button bound to the submitting state, and IME-safe reactive updates. Detail: Interactivity & Mounting.
Runtime tweaks: f.Input("Field").SetPlaceholder(...),
f.SetOptions("Field", ...), f.SetValues("Field", ...).
IsDirty()reports whether any field's current value differs from the baseline captured at creation or last load/reset (LoadValues,Reset,MarkPristine). Use this to gate persistence (e.g. auto-save).DirtyFields()returns the names of fields (in schema order) whose value differs from baseline, ornilwhen pristine. Use this for bulk edit to write only the fields the user actually changed, preventing untouched columns from being overwritten.
The library ships structure, the project owns the look (CSS-first doctrine).
The forms do not embed CSS or rely on custom styling libraries, but instead emit a standard semantic anatomy defined by webtyp/widget:
-
Stable anatomy and class contract — every bound field renders with the classes:
- Root container:
tw-field - Label:
tw-field__label - Inputs/Textareas/Selects/Datalists:
tw-field__input - Error spans:
tw-field__error - Radio group wrappers:
tw-field__radio-group
- Root container:
-
State attributes — validation and lock states are published as standard reactive
data-*attributes on the root container of each field:- Invalid state:
data-invalid="true" - Locked / Read-only state:
data-locked="true"
Global form skins (e.g.,
components/fieldset) use these selectors to style fields dynamically, e.g.,.tw-field[data-invalid="true"] .tw-field__error { ... }. - Invalid state:
-
form.SetGlobalClass("my-app-form")andf.SetClass("local-class")— adds classes to the<form>, useful for scoping:.my-app-form .tw-field { ... }.
Custom markup for custom inputs is possible by implementing form.Renderer;
see input/README.md.
18 types in input/ (full reference):
| Input | HTML type | Input | HTML type | |
|---|---|---|---|---|
Address |
text |
Password |
password |
|
Checkbox |
checkbox |
Phone |
tel |
|
Datalist |
text |
Radio |
radio |
|
Date |
date |
Rut |
text |
|
Email |
email |
Search |
search |
|
Filepath |
text |
Select |
select |
|
Gender |
radio |
Text |
text |
|
Hour |
time |
Textarea |
textarea |
|
IP |
text |
Number |
number |
Need one that isn't here? Custom inputs live in your own package: embed
input.Base, configure the Permitted rules, override Validate if needed.
Full pattern: input/README.md.
Creates a Form from any Fielder. idGen is required — form never
constructs its own ID generator (see model.IDGenerator); pass
unixid.NewUnixID() at your composition root, or a test double in tests.
Form id = parentID + "." + name, where the name comes from the optional
Namer interface (FormName() string, default "form").
| Method | Description |
|---|---|
String() string |
Generates form HTML |
Render() *dom.Element |
WASM — reactive DOM tree (dom.ViewRenderer) |
SetSSR(bool) *Form |
SSR mode: adds method/action attributes |
OnSubmit(func(model.Fielder, func(error))) *Form |
WASM submit callback |
Validate() error |
Validates all inputs, returns first error |
LoadValues(model.Fielder) error |
Populates every input from data, the inverse of SyncValues |
SyncValues(model.Fielder) error |
Copies input values back into the data struct |
ValidateData(byte, model.Fielder) error |
Server-side validation (crudp.DataValidator) |
Input(fieldName string) input.Input |
Returns the input for a field name |
SetOptions(fieldName, ...fmt.KeyValue) *Form |
Options for select/radio/datalist |
SetValues(fieldName, ...string) *Form |
Sets a value programmatically |
IsDirty() bool |
Reports whether any field's value differs from baseline |
DirtyFields() []string |
Returns names of fields differing from baseline (nil if none) |
Submit() error |
Runs sync + validate + OnSubmit callback programmatically; returns first validation error |
Reset() |
Clears all values and error messages |
NoResetOnSuccess() *Form |
Keeps values after a successful submit |
SubmitLabel(string) *Form |
Submit button text (default "Submit") |
SubmitLoadingLabel(string) *Form |
Button text while submitting (default label + "...") |
HideSubmit() *Form |
Renders without a submit button |
SetClass(...string) *Form |
Appends CSS classes to this form (on top of SetGlobalClass) |
GetID() string |
Form's HTML id |
Package-level: form.SetGlobalClass(classes ...string) — CSS classes for all
forms created afterwards.
form.New() iterates data.Schema(): a field becomes a form input iff its
Type (a model.Kind) also implements input.Input — capability by
interface, no registry, no name matching. Base kinds (model.Text(), …) are
skipped for rendering but still validate (fail-closed). Bound inputs are
positioned clones (Clone(formID, fieldName)) with constraint defaults
applied (NotNull → required) and current values bound from Pointers().
The input package stays free of dom imports (edge-safe); HTML rendering
is owned by form. Validation baselines come from model.Permitted
character whitelists — see API Reference.
- API Reference — Validate, SyncValues, Permitted detail
- Design & Architecture — core layers and philosophy
- Standard Types — the 18 input types in detail
- Interactivity & Mounting — WASM event handling
- Implementation Notes — contributors: library rules, file map, adding built-in inputs, test layout
- input/README.md — input package: custom inputs, composition,
Base