Form
One Form[T] serves both create and edit. Fields decode into your model,
rules run server-side, hooks receive *T, and edits write only the fields
that actually changed.
posts.Form(func(f *steward.Form[Post]) {
f.Text("Title").Rules("required|max:255")
f.Select("Status").Options(steward.Options{"draft": "Draft", "published": "Published"})
f.Markdown("Body")
f.Image("Cover").Dir("covers").MaxSize(2 << 20)
f.BelongsTo("AuthorID", "Author", "Name")
f.Datetime("PublishedAt")
})Field kinds
Text-ish: Text, Textarea, Email, Password, URL, Markdown,
Hidden, Display (read-only).
Numeric: Number, Decimal, Currency.
Choice: Select, Radio, Switch, Color.
Temporal: Date, Datetime, Time.
Uploads: File, Image — stored through the configured
Storage; Dir, MaxSize, Accept refine them.
Relations: BelongsTo(fkPath, relation, titleField) renders a searchable
select backed by the _options endpoint; MultiSelect(name) binds a
many-to-many pivot.
Layout: Divider() and Fieldset(title, func(*Form[T])).
Validation rules
Rules uses a compact pipe syntax; failures render inline (HTML) or as a
422 {"errors": {...}} (JSON):
f.Text("Slug").Rules("required|alpha_dash|max:64|unique:posts,slug,{id}")Available: required, max:n, min:n, email, url, integer,
numeric, alpha_dash, in:a,b,c, gte:n, lte:n, and
unique:table,column[,{id}] ({id} excludes the record being edited).
CreationRules / UpdateRules append rules for one mode only, and
Required() is shorthand for Rules("required").
Common field refinements: Default(v), Placeholder(s), Help(s),
ReadOnly(), Disable(), OnlyOnCreate() / OnlyOnUpdate(),
OptionsFunc(func(c) Options) for dynamic choices, and
SavingValue(func(c, raw) (any, error)) to transform input before decode.
Hooks
Typed, and able to veto:
f.Submitted(func(c *steward.Context) error { ... }) // before anything
f.Saving(func(c *steward.Context, m *Post) error { ... }) // validated, not yet persisted
f.Saved(func(c *steward.Context, m *Post, created bool) error { ... })
f.Deleting(func(c *steward.Context, ids []string) error { ... }) // veto deletes
f.Deleted(func(c *steward.Context, ids []string) error { ... })Returning an error from Submitted, Saving, or Deleting aborts the
operation and surfaces the message to the user.
Nested rows (hasMany)
Edit child records inside the parent form — added, changed, and removed rows are validated and persisted with the parent:
steward.HasMany(f, "Comments", "PostID", func(cf *steward.Form[Comment]) {
cf.Text("Author").Rules("required")
cf.Textarea("Body")
})"Comments" must be a has-many relation on the parent model, "PostID"
the child’s foreign key (set automatically). Child rows can’t contain
File/Image/BelongsTo fields yet — Verify() reports that at boot.