Skip to content

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.