form

package module
v0.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 5 Imported by: 1

README

tinywasm/form

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 tinywasm ecosystem — Go libraries for building full-stack web apps compiled to WebAssembly.

Install

go get github.com/tinywasm/form   # brings tinywasm/model as dependency

The code generator tooling is covered in step 2 of the Quick Start.

Quick Start

1. Define your model

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 (
    "github.com/tinywasm/model"
    "github.com/tinywasm/form/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
}
2. Generate the Fielder

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 tinywasm dev environment — it watches your model files and regenerates *_orm.go automatically with hot reload:

go install github.com/tinywasm/app/cmd/tinywasm@latest
tinywasm -tui    # interactive dev server (or -mcp for AI agents)

Manual alternative: run ormc (the standalone generator that tinywasm uses internally) once in your module:

go install github.com/tinywasm/ormc/cmd/ormc@latest
ormc
3. Create and render the form

Note 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 "github.com/tinywasm/form"

f, err := form.New("parent-id", &User{Name: "John"})  // "John" pre-fills the name input
html := f.String()          // SSR: render to HTML string
4. Make it interactive (WASM)

dom mounts components in the browser (compiled with TinyGo):

import "github.com/tinywasm/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", ...).

Styling

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 tinywasm/widget:

  1. 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
  2. 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 { ... }.

  3. form.SetGlobalClass("my-app-form") and f.SetClass("local-class") — adds classes to the <form>, useful for scoping: .my-app-form .tw-field { ... }.

Custom Inputs

Custom markup for custom inputs is possible by implementing form.Renderer; see input/README.md.

Built-in Input Types

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.

API Reference

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

Creates a Form from any Fielder. Form id = parentID + "." + name, where the name comes from the optional Namer interface (FormName() string, default "form").

Form Methods
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
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.

How It Works

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.

Documentation


Contributing


License

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RenderInput added in v0.2.7

func RenderInput(inp input.Input) *dom.Element

RenderInput is kept for backward compatibility and as a standalone helper

func SetGlobalClass added in v0.0.2

func SetGlobalClass(classes ...string)

Types

type Form

type Form struct {
	Inputs []input.Input
	// contains filtered or unexported fields
}

Form represents a form instance.

func New

func New(parentID string, data model.Fielder) (*Form, error)

New creates a new Form from a Fielder. parentID: ID of the parent DOM element where the form will be mounted. Returns an error if any exported field has no matching registered input.

func (*Form) Children added in v0.2.1

func (f *Form) Children() []dom.Component

Children returns the form's input fields as dom components (O(1), zero-alloc).

func (*Form) Focus added in v0.2.22

func (f *Form) Focus() *Form

Focus moves keyboard focus to the form's first field — a host UI calls this when entering an editable state (e.g. crudview's "+" / ⋮ Editar) so the user can start typing immediately instead of having to click into the form. A no-op if the form has no fields. Imperative, not reactive: the form's DOM already exists by the time a host unlocks it (this never runs on first mount), so a direct dom.Get+Focus is enough — no binding needed.

func (*Form) FocusedFieldID added in v0.2.22

func (f *Form) FocusedFieldID() string

FocusedFieldID returns the id Focus() last targeted (empty if never called, or the form has no fields). Real focus movement is a WASM-only DOM side effect (a no-op in the backend/SSR stub); this makes the INTENT observable in any build, e.g. for the view/conformance "New/Edit focuses the first field" clause to assert against without a live DOM.

func (*Form) GetID added in v0.0.17

func (f *Form) GetID() string

GetID returns the html id that group the form

func (*Form) HideSubmit added in v0.2.3

func (f *Form) HideSubmit() *Form

HideSubmit disables rendering of the submit button. Use this when the form is part of a larger UI that provides its own submit control (e.g. an external toolbar). Default is to render one.

func (*Form) Input added in v0.0.2

func (f *Form) Input(fieldName string) input.Input

Input returns the input with the given field name, or nil if not found.

func (*Form) IsDirty added in v0.2.24

func (f *Form) IsDirty() bool

IsDirty reports whether any field's current value differs from the baseline captured at the last load/reset (New, LoadValues, Reset). A host uses this to gate persistence — e.g. crudview's auto-save on field commit — so moving focus through a field without changing it never triggers a write. Comparing valueSignals directly (not a struct diff) keeps this exact and dependency-free: the signals are already the form's single source of truth for "current value" everywhere else in this package.

func (*Form) LoadValues added in v0.2.14

func (f *Form) LoadValues(data model.Fielder) error

LoadValues populates every input from data, the inverse of SyncValues. It is the operation a CRUD view needs when the user selects a record: one call, no per-field string conversion at the call site.

A nil data (including a typed-nil pointer inside the interface) resets the form — that is the "new record" case, not an error.

func (*Form) MarkPristine added in v0.2.24

func (f *Form) MarkPristine()

MarkPristine re-snapshots the baseline to the form's CURRENT values. A host calls this right after a successful save so a later field commit, with nothing further changed since that save, is not considered dirty again.

func (*Form) NoResetOnSuccess added in v0.2.4

func (f *Form) NoResetOnSuccess() *Form

NoResetOnSuccess disables the automatic form reset after a successful submit.

func (*Form) OnFieldChange added in v0.2.20

func (f *Form) OnFieldChange(fn func()) *Form

OnFieldChange registers a callback fired every time a field is committed by the user: blur for text/textarea/datalist, change for select/radio. This is the hook a host uses for auto-save (no explicit Save button) — the callback runs AFTER the field's own value/validate update, so the form's data is current.

func (*Form) OnSubmit added in v0.0.2

func (f *Form) OnSubmit(fn func(model.Fielder, func(error))) *Form

OnSubmit sets the callback for form submission in WASM mode.

func (*Form) ParentID added in v0.0.2

func (f *Form) ParentID() string

ParentID returns the ID of the parent element.

func (*Form) Render added in v0.2.11

func (f *Form) Render() *dom.Element

Render returns a reactive dom.Element tree for the form.

func (*Form) Reset added in v0.2.4

func (f *Form) Reset()

Reset clears all input values and error messages in the DOM and internal state.

func (*Form) SetClass added in v0.2.13

func (f *Form) SetClass(classes ...string) *Form

SetClass appends CSS classes to this form (on top of any global classes set via SetGlobalClass). Chainable.

func (*Form) SetID added in v0.0.17

func (f *Form) SetID(id string)

SetID sets the html id that group the form

func (*Form) SetLocked added in v0.2.20

func (f *Form) SetLocked(v bool) *Form

SetLocked gates every field to read-only/disabled (whole-form, not per-field) without discarding their values — used by a host UI to show an existing record before an explicit "edit" action unlocks it. Reactive: takes effect immediately on an already-rendered form.

func (*Form) SetOptions added in v0.0.2

func (f *Form) SetOptions(fieldName string, opts ...fmt.KeyValue) *Form

SetOptions sets options for the input matching the given field name.

func (*Form) SetSSR added in v0.0.2

func (f *Form) SetSSR(enabled bool) *Form

SetSSR enables or disables SSR mode for this form.

func (*Form) SetValues added in v0.0.2

func (f *Form) SetValues(fieldName string, values ...string) *Form

SetValues sets values for the input matching the given field name.

func (*Form) String added in v0.2.6

func (f *Form) String() string

String serializes the form to its HTML string representation.

func (*Form) Submit added in v0.2.13

func (f *Form) Submit() error

Submit runs the full submit pipeline programmatically: syncs input values into the bound struct, validates, and (if valid) fires the OnSubmit callback. Returns the first validation error, or nil if the submission was dispatched. The async result of the submission itself is delivered through the OnSubmit callback's done function.

func (*Form) SubmitLabel added in v0.2.3

func (f *Form) SubmitLabel(text string) *Form

SubmitLabel customizes the text on the submit button. If never called, the button shows "Submit".

func (*Form) SubmitLoadingLabel added in v0.2.4

func (f *Form) SubmitLoadingLabel(text string) *Form

SubmitLoadingLabel customizes the text on the submit button while submitting.

func (*Form) SyncValues added in v0.0.2

func (f *Form) SyncValues(data model.Fielder) error

SyncValues copies all input values back into the bound struct via the Fielder's Pointers() method.

func (*Form) Validate added in v0.0.2

func (f *Form) Validate() error

Validate validates all inputs and returns the first error found.

func (*Form) ValidateData added in v0.0.26

func (f *Form) ValidateData(action byte, data model.Fielder) error

ValidateData validates a Fielder instance using this form's input rules. Satisfies the updated crudp.DataValidator interface (with model.Fielder).

type Namer added in v0.0.29

type Namer interface {
	FormName() string
}

Namer is optionally implemented by Fielder types to provide a custom name. If not implemented, the form derives the name from the first Schema field's context.

type Renderer added in v0.2.13

type Renderer interface {
	RenderInput(value *dom.SignalString, onInput func(string)) *dom.Element
}

Renderer is an optional capability for custom inputs that own their markup. The form still owns the field wrapper (div.tw-field), the error span, and validation: the widget must call onInput with the new value on user input — the form updates the value signal and runs live validation. The value signal carries the initial value and programmatic updates (SetValues).

Directories

Path Synopsis
example
web command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL