form

package module
v0.2.13 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 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.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):

  1. Stable class contract — every bound field renders as:

    <div class='tw-field'>
      <input ... />
      <span class='tw-field-error' aria-live='polite'></span>
    </div>
    

    Hook classes: tw-field, tw-field-error, tw-field-error--visible, tw-radio-group. Override them in your project stylesheet to theme every form at once.

  2. RenderCSS() (!wasm) — returns the base styles as an additive css.Stylesheet. You don't wire it manually: the tinywasm SSR pipeline discovers package-level RenderCSS() functions in your imports and bundles them into the initial HTML automatically. Your overrides live in the project's CSS entry point — by convention config/css.go at the project root — where RootCSS() declares token overrides and your own rules win the cascade:

    // config/css.go
    //go:build !wasm
    
    package config
    
    import "github.com/tinywasm/css"
    
    func RootCSS() *css.Stylesheet {
        return css.Root(
            css.Declare(css.ColorPrimary, "#FF6B35"),
            // ...your theme tokens; add Rule(".tw-field", ...) overrides here too
        )
    }
    

    See tinywasm/css for the token/theming contract.

  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
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 RenderCSS added in v0.2.4

func RenderCSS() *css.Stylesheet

RenderCSS returns the form's CSS contribution (additive — see tinywasm/css contract). Call from the project's css.go aggregate so assetmin picks it up.

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) 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) NoResetOnSuccess added in v0.2.4

func (f *Form) NoResetOnSuccess() *Form

NoResetOnSuccess disables the automatic form reset after a successful submit.

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) 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