Help us shape the future of Go! Take the 2026 Go Developer Survey

forms

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 30 Imported by: 0

README

Forms

Contact forms, quote requests and other forms that editors build in the admin, with an inbox for the answers and an optional e-mail for each one.

Each instance holds one form. Add an instance per form: a site can have contact and quote side by side, each with its own fields, inbox and template.

Add it to the site

app.Use(
    forms.Module("contact", "Contact"),
    forms.Module("quote", "Quote request"),
)

The key (contact) is the instance's name in URLs and templates and must be unique on the site. The title (Contact) is what editors see in the admin navigation and the page editor.

Build the form

In the admin, open the instance (/admin/contact; /admin is the default admin path) and choose New form. An instance has one form, so the button disappears once it exists. The form has a title, an introduction and a success message (both Markdown), its fields, and an optional Notification e-mail.

Fields are written as text, one per line: name | type | label | placeholder | help. A * after the name makes the field required; placeholder and help are optional. The options of a select follow on their own lines, starting with -:

name* | text | Name
email* | email | E-mail | you@example.com
topic | select | Topic
- sales | Sales
- support | Support
message* | textarea | Message | | Tell us what you need.
terms* | checkbox | I agree to the privacy statement
  • Types: text, email, textarea, select, checkbox, number and date.
  • Names use lowercase letters, digits and underscores, start with a letter, and are unique. website is taken by the spam trap.
  • A form has at most 50 fields, a select at most 100 options.
  • Names, types and options are shared by all languages; labels, placeholders, help and option labels belong to the language being edited. A field without a label in the visitor's language shows its name.

Publish the form when it is ready. It is shown in each language that has a title, a success message and field labels.

While the form is on a page, links to addresses on the site in its introduction and success message count for Links to a page: the editor of a page lists the form when it links there, and Pages › Broken links lists its links that lead nowhere.

Place it on a page

In the admin, open a page and choose the instance under Shows on this page. The page shows the form at its own address and receives it there too, with contact/form.gohtml, or forms/form.gohtml for every form without a design of its own. Generate a complete working starting point, with every kind of field:

task templates -- contact

A minimal template:

{{/* @dot github.com/donseba/go-webthings/modules/forms.FormView */}}
<h1>{{.Title}}</h1>

{{if .Sent}}
    {{.Success}}
{{else}}
    {{.Intro}}

    {{if .Throttled}}<p>You sent this form several times already. Please try again later.</p>{{end}}
    {{if .Invalid}}<p>Please check the marked fields.</p>{{end}}

    {{form_render .Form .Errors}}
{{end}}

.Form is the form as the editor built it, in the page's language. form_render renders all of it with go-form: each field with its label, placeholder, help text and limits, the visitor's answers and the errors of a post at their fields, the spam trap and a Send button, translated with the site's translations and in the site's form theme. The fields' ids start with the instance key, such as contact-email.

To lay out the fields yourself, write the form with form_open, form_field per name in .Fields and form_close:

{{form_open .Form}}
    {{range .Fields}}
        {{form_field $.Form . $.Errors}}
    {{end}}
    <button type="submit">Send</button>
{{form_close}}

form_field renders a field by its name, email for example, so a template can also place the fields one by one. See Template functions.

Visitors

The form posts to the page itself (.Action), not to a visitor action under /_/, but it is protected the same way.

  • With errors, the page shows the form again with status 422, the visitor's answers, .Invalid set and .Errors: go-form's messages in the page's language, which form_render and form_field show at their fields.
  • After a valid submission the visitor returns to the page with ?sent=1, such as /en/contact?sent=1, and .Sent is set: show .Success instead of the form.
  • form_open and form_render add the spam trap. Bots that fill it in are told the form was sent; nothing is stored.
  • Posts from other sites are refused by cross-origin protection. The form needs no token or cookie, so it never expires, however long it stays open.
  • Each visitor may send the form 10 times in 10 minutes, posts with errors included. After that the form is shown again with .Throttled set and their answers kept, with status 429; nothing is stored.

Values are checked by type, with go-form's validation: a text is one line of at most 500 characters, a textarea at most 10,000, an email one address mail can be sent to, as the site's other forms check it (mail.Rule, see Addresses in forms), so anna@localhost is refused, a number a number, a date a YYYY-MM-DD date and a select one of its options. A checkbox is stored as yes or no.

Inbox

Submissions on the form's admin page opens the inbox (/admin/contact/inbox), with tabs for new, archived and all submissions, dated in the site's timezone. The badge in the navigation counts the new ones.

A submission keeps the answers with the labels the visitor saw and the language they used, so later changes to the form leave it unchanged. Editors archive a handled submission or move it back to new; Move to trash moves it to the trash, where it can be restored or deleted for good. Archiving needs the edit right, deleting the delete right. Trash on the inbox lists the deleted submissions.

E-mail

When the form has a notification address, every submission is mailed there in the admin's language. By default the subject is the form's title in the visitor's language; the text lists each answer under its label, followed by a link to the submission in the admin. Replying goes to the first address the visitor gave in an email field.

The mail is the template notify, with a forms.NotifyView. A theme replaces it with mail/contact/notify.gohtml, or mail/forms/notify.gohtml for every form; task templates -- contact writes the default there. The submission is stored first; a mail that fails is only logged.

From Go

forms.Of(app, "contact") returns the instance once the app has started. It reads and handles submissions the way the inbox does:

contact := forms.Of(app, "contact")

waiting, err := contact.Count(ctx, forms.StatusNew)
latest, more, err := contact.SubmissionsByStatus(ctx, forms.StatusNew, 0, 20)

An empty status lists or counts every submission. Submission reads one, and Archive, Unarchive, DeleteSubmission, UndeleteSubmission and PurgeSubmission take a submission ID; the last three move a submission to the trash, bring it back or remove it for good. For seed data, ParseFields reads fields in the editor's syntax and Store() saves the form:

fields, labels, err := forms.ParseFields("name* | text | Name\nemail* | email | E-mail\nmessage* | textarea | Message\n")

form, err := contact.Store().Save(ctx, content.Draft[forms.Definition, forms.Text]{
    Fields: forms.Definition{Fields: fields, Notify: "info@example.com"},
    Translations: content.SetText(nil, "en", forms.Text{
        Title:   "Contact",
        Success: "Thank you. We reply within two working days.",
        Labels:  labels,
    }),
})

err = contact.Store().Publish(ctx, form.ID, "", "en")
  • Registrations handles sign-ups for events, with places, prices and a confirmation mail.
  • Submissions lets visitors send in news that editors accept as a draft post.
  • Newsletter collects e-mail addresses with double opt-in.

Documentation

Overview

Package forms adds a public form, such as a contact form or a quote request, with an inbox for its submissions. Each instance is one form; add one instance per form and place it on a page:

app.Use(forms.Module("contact", "Contact"), forms.Module("quote", "Quote request"))

Editors define the fields in the admin, one per line (see the Fields help text in the editor), with labels per language.

The website renders forms/form.gohtml with a FormView as its data. A minimal template:

{{/* @dot github.com/donseba/go-webthings/modules/forms.FormView */}}
<h1>{{.Title}}</h1>
{{if .Sent}}{{.Success}}{{else}}
  {{.Intro}}
  {{form_render .Form .Errors}}
{{end}}

form_render renders every field with go-form (see FormView.Form): its label, help text, placeholder and limits, the visitor's answer and the errors of a post at their fields, in the page's language, with the spam trap and a Send button. A template that lays out the fields itself uses {{form_open .Form}}, {{form_field .Form name .Errors}} per name of Fields and {{form_close}}.

The form posts to its own page, so a post with errors shows the form again with the visitor's answers, Errors and Invalid set. Like the visitor actions of other modules (see webthings.Env.Actions) it rejects cross-site posts (http.CrossOriginProtection), tells bots that fill in the trap that the form was sent without storing anything, and accepts 10 posts per visitor in 10 minutes: after that the form is shown again with Throttled set and nothing stored. After a valid submission the visitor returns to the page with ?sent=1 and Sent is set.

When the form has a notification address, every submission is mailed there with the answers and a link to it in the admin; replying goes to the visitor's address when the form has an email field. The mail is the mail template notify (see webthings.Module.Mail) with NotifyView as data, in the admin's language; a theme replaces it with mail/<key>/notify.gohtml or mail/forms/notify.gohtml. Submissions arrive as new and can be archived in the admin once handled.

Index

Constants

View Source
const (
	// StatusNew is a submission no editor has handled yet; the admin
	// counts them in a badge.
	StatusNew = "new"
	// StatusArchived is a submission an editor has handled.
	StatusArchived = "archived"
)

Statuses of a submission: new until an editor archives it.

Variables

This section is empty.

Functions

func Module

func Module(key, title string) webthings.Module

Module adds a form. key is used in URLs of the admin and must be unique on the site; title is shown in the admin and the page editor.

func ParseFields

func ParseFields(source string) (FieldList, Labels, error)

ParseFields reads fields in the editor's syntax, for defining a form in code (for example in a seed). The labels belong to one language.

Types

type Answer

type Answer struct {
	Name  string `json:"name"`
	Label string `json:"label"`
	Value string `json:"value"`
}

Answer is one submitted value, with the label the visitor saw.

type Answers

type Answers []Answer

Answers is stored as JSON in one column.

func (*Answers) Scan

func (a *Answers) Scan(src any) error

Scan reads answers stored by Value.

func (Answers) Value

func (a Answers) Value() (driver.Value, error)

Value stores the answers as JSON.

type Definition

type Definition struct {
	Fields FieldList `db:"form_fields"`
	Notify string    `db:"notify"`
}

Definition is the language-independent part of a form. Notify is the address new submissions are mailed to; empty sends no mail.

type Field

type Field struct {
	Name     string   `json:"name"`
	Type     string   `json:"type"`
	Required bool     `json:"required,omitempty"`
	Options  []string `json:"options,omitempty"`
}

Field is one input of the form. Labels are translated; see Label.

type FieldList

type FieldList []Field

FieldList is stored as JSON in one column.

func (*FieldList) Scan

func (f *FieldList) Scan(src any) error

Scan reads fields stored by Value.

func (FieldList) Value

func (f FieldList) Value() (driver.Value, error)

Value stores the fields as JSON.

type FormView

type FormView struct {
	Title string
	Intro template.HTML
	// Action is the URL the form posts to: the page itself.
	Action string
	// Form is the form's model: its fields as the editor defined them, in
	// the page's language, with the visitor's answers after a post with
	// errors, for go-form's template functions, such as {{form_render
	// .Form .Errors}}, or {{form_open .Form}} with {{form_field .Form name
	// .Errors}} per name of Fields. form_open adds the spam trap
	// (frontend.Honeypot), and the fields' ids start with the instance key.
	Form form.RenderModel
	// Fields are the names of the form's fields, in order, for form_field.
	Fields []string
	// Errors are the problems of a post per field, with messages in the
	// page's language, which form_field and form_render show at their
	// fields; empty otherwise.
	Errors form.FieldErrors
	// Invalid is set when a submission has errors; see Errors.
	Invalid bool
	// Throttled is set when the visitor sent the form too often in a short
	// time. Nothing was stored; the form is shown with their answers, to send
	// again later.
	Throttled bool
	// Sent is set after a successful submission; show Success instead of the form.
	Sent    bool
	Success template.HTML
}

FormView is the data of forms/form.gohtml.

type InboxRow

type InboxRow struct {
	URL      string
	Date     string
	Locale   string
	Summary  string
	Archived bool
}

InboxRow is one submission in the inbox; Summary shows its first answers.

type InboxView

type InboxView struct {
	Title   string
	BackURL string
	// TrashURL shows the deleted submissions in the admin's trash.
	TrashURL string
	// Filters switch between new, archived and all submissions.
	Filters admin.Tabs
	Rows    []InboxRow
	Pager   admin.Pager
}

InboxView is the data of templates/admin/inbox.gohtml: the submissions of a form, filtered by status.

type Item

type Item = content.Item[Definition, Text]

Item is a stored form with its translations.

type Label

type Label struct {
	Label       string            `json:"label"`
	Placeholder string            `json:"placeholder,omitempty"`
	Help        string            `json:"help,omitempty"`
	Options     map[string]string `json:"options,omitempty"`
}

Label is the text of one field in one language. Options maps option values to their labels.

type Labels

type Labels map[string]Label

Labels maps field names to their text; stored as JSON in one column.

func (*Labels) Scan

func (l *Labels) Scan(src any) error

Scan reads labels stored by Value.

func (Labels) Value

func (l Labels) Value() (driver.Value, error)

Value stores the labels as JSON.

type NotifyView

type NotifyView struct {
	// Title is the form's title in the language the visitor used.
	Title string
	// Locale is the language the visitor used.
	Locale string
	// Answers are the submitted values, each with the label the visitor saw.
	Answers Answers
	// ReplyTo is the first address the visitor gave in an email field; empty
	// when there is none.
	ReplyTo string
	// SubmissionURL opens the submission in the admin.
	SubmissionURL string
}

NotifyView is the data of the mail template notify, sent to the form's notification address in the admin's language. Replying answers the visitor when the form has an email field.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is one form, for the website's own code.

func Of

func Of(app *webthings.App, key string) *Service

Of returns the Forms instance with key from a started app, or nil.

func (*Service) Archive

func (s *Service) Archive(ctx context.Context, id string) error

Archive marks a submission as handled; it leaves the list of new ones.

func (*Service) Count

func (s *Service) Count(ctx context.Context, status string) (int, error)

Count returns the number of submissions with status; an empty status counts all.

func (*Service) DeleteSubmission

func (s *Service) DeleteSubmission(ctx context.Context, id string) error

DeleteSubmission moves a submission to the trash: it leaves the inbox and the counts, and UndeleteSubmission brings it back. It returns db.ErrNotFound when the submission is not in the inbox.

func (*Service) PurgeSubmission

func (s *Service) PurgeSubmission(ctx context.Context, id string) error

PurgeSubmission removes a submission in the trash for good. It returns db.ErrNotFound when it is not in the trash.

func (*Service) Store

func (s *Service) Store() content.Store[Definition, Text]

Store gives write access to this instance's form definition, for imports and seeds. Admin edits go through the same store.

func (*Service) Submission

func (s *Service) Submission(ctx context.Context, id string) (Submission, error)

Submission returns one submission that is not in the trash, or db.ErrNotFound.

func (*Service) SubmissionsByStatus

func (s *Service) SubmissionsByStatus(ctx context.Context, status string, offset, limit int) ([]Submission, bool, error)

SubmissionsByStatus returns the submissions with one status, StatusNew or StatusArchived, newest first, skipping offset, and whether older ones exist; an empty status lists all. A limit of 0 or less returns every submission after offset.

func (*Service) TrashedSubmissions

func (s *Service) TrashedSubmissions(ctx context.Context, limit, offset int) ([]Submission, int, error)

TrashedSubmissions returns submissions in the trash, most recently deleted first: at most limit of them from offset (all for a limit of 0), and how many the trash holds in all.

func (*Service) Unarchive

func (s *Service) Unarchive(ctx context.Context, id string) error

Unarchive returns an archived submission to the new ones.

func (*Service) UndeleteSubmission

func (s *Service) UndeleteSubmission(ctx context.Context, id string) error

UndeleteSubmission brings a submission back from the trash, with the status it had. It returns db.ErrNotFound when it is not in the trash.

type Submission

type Submission struct {
	ID        string     `db:"id"`
	Locale    string     `db:"locale"`
	Status    string     `db:"status"`
	Answers   Answers    `db:"answers"`
	CreatedAt time.Time  `db:"created_at"`
	DeletedAt *time.Time `db:"deleted_at"`
	DeletedBy db.Ref     `db:"deleted_by"`
}

Submission is one sent form. DeletedAt and DeletedBy are set for submissions in the trash (see TrashedSubmissions).

type SubmissionView

type SubmissionView struct {
	Title    string
	BackURL  string
	Date     string
	Locale   string
	Archived bool
	Answers  Answers
	// Archive or Unarchive is set, depending on the status, for editors.
	Archive   *form.RenderModel
	Unarchive *form.RenderModel
	Delete    *form.RenderModel
}

SubmissionView is the data of templates/admin/submission.gohtml: one submission with every answer.

type Text

type Text struct {
	Title   string `db:"title"`
	Intro   string `db:"intro"`
	Success string `db:"success"`
	Labels  Labels `db:"field_labels"`
}

Text is a form in one language. Intro and Success are Markdown.

Jump to

Keyboard shortcuts

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