htmlform

package module
v0.0.0-...-1fb9234 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 18 Imported by: 0

README

go-htmlform

Package htmlform generates an HTML form for a Go function wrapped with go-function and calls the function with the submitted values.

go get github.com/domonda/go-htmlform

It was the htmlform module of go-function until htmlform/v0.7.1; change the import path github.com/domonda/go-function/htmlform to github.com/domonda/go-htmlform, see CHANGELOG.md.

Scope

htmlform is meant for quick internal dashboards and admin tools, like a form for triggering a maintenance task or entering test data. It is not meant for polished production websites: the generated forms have a plain default layout, and the package doesn't have the same quality criteria as the root package github.com/domonda/go-function.

For public websites, write the forms yourself and call your functions through httpfun or the wrappers of the root package.

Usage

import (
	"github.com/domonda/go-function"
	"github.com/domonda/go-function/httpfun"
	"github.com/domonda/go-htmlform"
)

func CreateUser(ctx context.Context, name, email string, age int) error {
	// ...
	return nil
}

handler := htmlform.MustNewHandler(
	function.MustReflectWrapper(CreateUser, "ctx", "name", "email", "age"),
	"Create User",
	httpfun.RespondStaticHTML("<h1>User created</h1>"),
)
http.Handle("/create-user", handler)

A GET request shows the form, a POST request calls CreateUser with the parsed values. Submissions are protected against cross-site request forgery and limited in size. Arguments of file types like fs.File of go-fs get upload fields.

See the package documentation for field types, validators, options, custom templates and the security notes, docs/reference-http-status-codes.md for every response status, and examples/main.go for a runnable example.

Development

tools/go.mod pins the development tools, so their dependencies don't become dependencies of this module:

go test ./...
go generate ./examples  # regenerate the example wrappers
go tool -modfile=tools/go.mod gosec ./...

Documentation

Overview

Package htmlform provides utilities for automatically generating HTML forms from function signatures.

Overview

The htmlform package enables you to create web forms that automatically generate HTML input fields based on a function's parameters. When the form is submitted, the values are parsed and passed to the function, making it easy to create web interfaces for any Go function.

Scope

This package is meant for quick internal dashboards and admin tools, not for polished production websites, and it doesn't have the same quality criteria as the root package github.com/domonda/go-function. For public websites, write the forms yourself and call your functions through httpfun or the wrappers of the root package.

Basic Usage

Generate an HTML form from a function:

func CreateUser(ctx context.Context, name, email string, age int, active bool) error {
    fmt.Printf("Creating user: %s (%s), age %d, active: %v\n", name, email, age, active)
    return nil
}

func main() {
    wrapper := function.MustReflectWrapper(CreateUser, "ctx", "name", "email", "age", "active")
    handler := htmlform.MustNewHandler(
        wrapper,
        "Create New User",           // Form title
        httpfun.RespondStaticHTML("<h1>User created successfully!</h1>"),
    )

    http.Handle("/create-user", handler)
    http.ListenAndServe(":8080", nil)
}

When users visit /create-user:

  • GET request: Displays an HTML form with fields for name, email, age, and active
  • POST request: Submits the form, calls CreateUser, and displays success message

Automatic Field Type Detection

The package automatically selects appropriate HTML input types based on Go types:

string         → <input type="text">
int, uint      → <input type="number">
float          → <input type="number" step="any">
bool           → <input type="checkbox">
fs.FileReader  → <input type="file">
other types    → <input type="text">

Types that parse their own strings, like time.Duration ("5s") and types implementing encoding.TextUnmarshaler, get text fields also if their kind is a number. Set input types like "datetime-local" or "date" for time.Time with SetArgInputType, default values of time.Time are then formatted for these inputs. Arguments named _ get no form field and are passed as zero values.

Customizing Fields

You can customize form fields using the Handler methods:

handler := htmlform.MustNewHandler(wrapper, "User Form", resultWriter)

// Set field as required (overrides type-based detection)
handler.SetArgRequired("email", true)

// Set default value
handler.SetArgDefaultValue("age", 18)
handler.SetArgDefaultValue("active", true)

// Set custom input type
handler.SetArgInputType("email", "email")
handler.SetArgInputType("bio", "textarea")
handler.SetArgInputType("password", "password")

// Add dropdown options
handler.SetArgOptions("role", []htmlform.Option{
    {Label: "User", Value: "user"},
    {Label: "Admin", Value: "admin"},
    {Label: "Guest", Value: "guest"},
})

// Customize submit button
handler.SetSubmitButtonText("Create User")

Field Validation

The SetArg* methods of Handler panic for names that are not arguments of the wrapped function, so a typo or a renamed argument can't silently remove a validator or another setting.

Add custom validators for form fields:

handler.SetArgValidator("email", func(ctx context.Context, value any) error {
    email, ok := value.(string)
    if !ok || !strings.Contains(email, "@") {
        return errors.New("invalid email address")
    }
    return nil
})

handler.SetArgValidator("age", func(ctx context.Context, value any) error {
    age, ok := value.(int)
    if !ok || age < 18 {
        return errors.New("must be 18 or older")
    }
    return nil
})

Validators are called with the context of the request and the argument values parsed from the submitted form, or the zero value of an argument that was not submitted. The function is called with exactly these values. If a validator returns an error, then the function is not called and a 400 Bad Request error naming the argument is passed to the result writer. The text of the validator error is shown to the client in that response.

Required Fields

Fields are automatically marked as required based on their type:

  • Non-pointer types like int → Required by default
  • Pointer types (*int, *string) → Optional
  • String type → Optional (empty string is valid)
  • Types implementing IsNull() → Optional
  • bool checkboxes are never marked as required, unchecked is false
  • A variadic last argument → Optional, like in Go it can have no values

Required fields are enforced by browsers with the required attribute of the form field, the server passes the zero value for a missing value.

Override automatic detection:

handler.SetArgRequired("email", true)   // Force required
handler.SetArgRequired("age", false)    // Make optional

Custom Form Templates

The default form template can be customized by assigning a custom html/template text to FormTemplate before creating handlers, because NewHandler parses FormTemplate:

htmlform.FormTemplate = `<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
    <h1>{{.Title}}</h1>
    <form method="post" enctype="multipart/form-data">
        {{range .Fields}}
            <label>{{.Label}}</label>
            {{if eq .Type "checkbox"}}
                <input type="checkbox" name="{{.Name}}" value="true" {{if eq .Value "true"}}checked{{end}}>
            {{else}}
                <input type="{{.Type}}" name="{{.Name}}" value="{{.Value}}">
            {{end}}
        {{end}}
        <button>{{.SubmitButtonText}}</button>
    </form>
</body>
</html>`
handler, err := htmlform.NewHandler(wrapper, "My Form", resultWriter)

File Uploads

Function arguments with a type implementing fs.FileReader, like fs.File or fs.FileReader, automatically get file upload fields. They are only taken from files uploaded with the form and passed as fs.File of the multipart form file, which is only readable while the request is handled. Without an uploaded file the argument is the zero value, which is nil for fs.FileReader. Pointers, slices and arrays of such types are file arguments too: a slice or array gets all files uploaded with the same field name, also files with the same name, which go-fs names like "scan (2).pdf", and its form field allows selecting multiple files. A non-empty text value submitted for a file argument is rejected with 400 Bad Request without calling the function, because it would be used as path of a file on the server. A file argument of a type that fs.File can't be assigned to, like fs.MemFile or a pointer to a slice of files, is an error of NewHandler, and so is a file type in a struct field or map of an argument, which uploads can't fill and a text value would set to a path on the server.

func ProcessDocument(ctx context.Context, name string, document fs.FileReader) error {
    data, err := document.ReadAll(ctx)
    if err != nil {
        return err
    }
    fmt.Printf("Processing %s: %d bytes\n", name, len(data))
    return nil
}

handler := htmlform.MustNewHandler(
    function.MustReflectWrapper(ProcessDocument, "ctx", "name", "document"),
    "Upload Document",
    httpfun.RespondStaticHTML("<p>Document processed!</p>"),
)

The form will include a file input field for the document parameter.

WARNING: Never expose functions with fs.File or fs.FileReader arguments to untrusted input through string arguments of httpfun handlers or cli commands, or through JSON (CallWithJSON, httpfun.RequestBodyJSONFieldsAsArgs, or a -replaceForJSON of gen-func-wrappers to fs.File): a string becomes the path of a file on the server that the function then reads or writes on behalf of the client. For the same reason don't register a process-global StringScanner that converts strings to fs.FileReader. NewHandler rejects arguments with file types in struct fields and maps, but can't detect types that read a file path in their own UnmarshalJSON or UnmarshalText method.

Complete Example

package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/domonda/go-function"
    "github.com/domonda/go-htmlform"
    "github.com/domonda/go-function/httpfun"
)

func RegisterUser(ctx context.Context, username, email, password string, age int, newsletter bool) error {
    fmt.Printf("Registering: %s (%s), age %d, newsletter: %v\n",
        username, email, age, newsletter)
    return nil
}

func main() {
    wrapper := function.MustReflectWrapper(RegisterUser, "ctx", "username", "email", "password", "age", "newsletter")
    handler := htmlform.MustNewHandler(
        wrapper,
        "User Registration",
        httpfun.RespondStaticHTML("<h2>Registration successful!</h2>"),
    )

    // Customize fields
    handler.SetArgInputType("email", "email")
    handler.SetArgInputType("password", "password")
    handler.SetArgRequired("email", true)
    handler.SetArgDefaultValue("age", 18)
    handler.SetSubmitButtonText("Register")

    http.Handle("/register", handler)
    fmt.Println("Server running on http://localhost:8080/register")
    http.ListenAndServe(":8080", nil)
}

Result Handling

After form submission, use any httpfun.ResultsWriter to handle the response:

// Simple success message
httpfun.RespondStaticHTML("<h1>Success!</h1>")

// JSON response
httpfun.RespondJSON

// Redirect after success
httpfun.RespondRedirect("/thank-you")

// Custom response based on results
httpfun.ResultsWriterFunc(func(results []any, err error, w http.ResponseWriter, r *http.Request) error {
    if err != nil {
        return err
    }
    fmt.Fprintf(w, "<p>Created user with ID: %v</p>", results[0])
    return nil
})

Request Size Limit

The body of a form submission including uploaded files is limited to 100 MiB, larger requests are answered with 413 Request Entity Too Large. Uploaded files larger than httpfun.MaxMultipartFormMemory are buffered in temporary files instead of memory:

handler.SetMaxRequestBodySize(10 << 20) // 10 MiB

Cross-Site Request Forgery Protection

Form submissions are protected against cross-site request forgery (CSRF) with http.CrossOriginProtection: POST requests that browsers send from other origins are answered with 403 Forbidden without calling the function. Requests of non-browser clients without the Sec-Fetch-Site and Origin headers are accepted. Configure trusted origins or disable the protection with nil:

protection := http.NewCrossOriginProtection()
err := protection.AddTrustedOrigin("https://example.com")
if err != nil {
    panic(err)
}
handler.SetCrossOriginProtection(protection)

Error Handling

The handler automatically:

  • Catches panics and displays them as HTTP errors
  • Validates form submissions
  • Handles multipart form data (including file uploads)
  • Returns appropriate HTTP status codes

Errors from the wrapped function are passed to the result writer, which can handle them appropriately.

Styling

The default template includes minimal CSS styling. Customize FormTemplate to add your own styles, the title is HTML escaped and can't be used to inject CSS:

htmlform.FormTemplate = strings.Replace(htmlform.FormTemplate,
    "</style>",
    "form { max-width: 600px; margin: 0 auto; }\n</style>",
    1,
)
handler := htmlform.MustNewHandler(wrapper, "My Form", resultWriter)

Best Practices

  • Use context.Context as the first parameter for cancellation support
  • Return error as the last result for proper error handling
  • Set sensible default values for optional fields
  • Use appropriate input types (email, password, url, tel, etc.)
  • Provide clear labels through function documentation
  • Add validators for complex validation logic
  • Use dropdown options for fields with limited choices
  • Test file upload handling with size limits
  • Customize the success response to match your application style

Limitations

  • Context.Context parameters are automatically excluded from the form
  • Maps other than map[string]any are not supported, struct and map[string]any arguments are entered as JSON in a text field
  • Slices and arrays of files get a multiple file upload field, other slices and arrays are entered as one text value like [1,2,3], or as multiple values of the field in a custom template, like of a select with the multiple attribute
  • A variadic argument is not required, because it can have no values
  • time.Time arguments get a text field unless their input type is set to "datetime-local" with SetArgInputType
  • The server only validates with the validators set by SetArgValidator and by parsing the values, required fields are only enforced by browsers

Thread Safety

Handler instances are safe for concurrent use after configuration. Do not modify handler settings (SetArgRequired, etc.) after the handler starts serving requests.

Index

Constants

This section is empty.

Variables

View Source
var FormTemplate = `` /* 2982-byte string literal not displayed */

FormTemplate is the default HTML template used to render forms. It supports multiple input types including text, number, checkbox, select, textarea, and file.

The template expects a struct with:

  • Title: Page title and heading
  • Fields: Slice of formField (Name, Label, Type, Value, Required, Multiple, Step, Options)
  • SubmitButtonText: Text for the submit button

You can customize this template by assigning your own html/template text before creating handlers, because NewHandler parses FormTemplate.

The template handles these field types specially:

  • checkbox: Inline label, checked state support
  • select: Dropdown with options, selected value support
  • radio: One radio button per option, checked value support
  • textarea: Multi-line text with 40 cols × 5 rows
  • all others: Standard <input> tags with type attribute

Example customization:

htmlform.FormTemplate = myCustomTemplate
handler, err := htmlform.NewHandler(wrapper, "My Form", resultWriter)

Functions

This section is empty.

Types

type ArgValidator

type ArgValidator func(ctx context.Context, value any) error

ArgValidator validates the value of a function argument parsed from a submitted form before the function is called.

The ctx is the context of the HTTP request. The value has the type of the function argument and is the zero value of that type if the argument was not submitted with the form.

Returning an error rejects the form submission with 400 Bad Request, and the error text is shown to the client in the response.

type Handler

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

Handler is an http.Handler that generates and processes HTML forms for a wrapped function. It automatically creates form fields based on function parameters and handles form submission.

The handler responds to:

  • GET requests: Displays the HTML form
  • POST requests: Processes form submission and calls the wrapped function

POST requests are protected against cross-site request forgery, see SetCrossOriginProtection, and limited in size, see SetMaxRequestBodySize.

Example usage:

func CreateUser(ctx context.Context, name, email string, age int) error {
    // Implementation
    return nil
}

handler := htmlform.MustNewHandler(
    function.MustReflectWrapper(CreateUser, "ctx", "name", "email", "age"),
    "Create User",
    httpfun.RespondStaticHTML("<h1>Success!</h1>"),
)
http.Handle("/create-user", handler)

func MustNewHandler

func MustNewHandler(fun function.Wrapper, title string, successHandler httpfun.ResultsWriter) (handler *Handler)

MustNewHandler is like NewHandler but panics on error. Use this in initialization code where form creation failures should be fatal.

func NewHandler

func NewHandler(wrappedFunc function.Wrapper, title string, resultWriter httpfun.ResultsWriter) (handler *Handler, err error)

NewHandler creates a new form handler for the given wrapped function.

Parameters:

  • wrappedFunc: The function to generate a form for
  • title: The HTML page title and form heading
  • resultWriter: Handler for the function's results after form submission

A nil resultWriter writes nothing for a successful submission like httpfun.RespondNothing, errors are answered by httpfun.HandleError.

Returns an error if the form template fails to parse, or if an argument can't be passed with a form: a file argument of a type that an uploaded fs.File can't be assigned to, like fs.MemFile or a pointer to a slice of files, or an argument with a file type in a struct field or map, which an upload can't fill and a text value would set to the path of a file on the server.

Example:

handler, err := htmlform.NewHandler(
    wrapper,
    "User Registration",
    httpfun.RespondJSON,
)

func (*Handler) ServeHTTP

func (handler *Handler) ServeHTTP(response http.ResponseWriter, request *http.Request)

ServeHTTP implements http.Handler. It handles both GET requests (display form) and POST requests (process form submission). Panics are recovered and converted to HTTP errors, except http.ErrAbortHandler, with which net/http aborts the response.

func (*Handler) SetArgDefaultValue

func (handler *Handler) SetArgDefaultValue(arg string, value any)

SetArgDefaultValue sets the default value displayed in the form field.

Example:

handler.SetArgDefaultValue("age", 18)
handler.SetArgDefaultValue("active", true)
handler.SetArgDefaultValue("country", "US")

Panics if arg is not the name of an argument of the wrapped function, so that a typo or renamed argument doesn't silently remove the setting.

func (*Handler) SetArgInputType

func (handler *Handler) SetArgInputType(arg string, value string)

SetArgInputType overrides the automatically detected HTML input type.

Supported types include:

  • "text", "email", "password", "url", "tel", "search"
  • "number", "range"
  • "date", "datetime-local", "time", "month", "week"
  • "checkbox"
  • "radio" with the options set by SetArgOptions
  • "file", only for file arguments
  • "textarea"
  • "color"

Example:

handler.SetArgInputType("email", "email")
handler.SetArgInputType("password", "password")
handler.SetArgInputType("bio", "textarea")
handler.SetArgInputType("age", "range")

Panics if arg is not the name of an argument of the wrapped function, so that a typo or renamed argument doesn't silently remove the setting, and for "file" if the argument doesn't take uploaded files, because the upload would be ignored.

func (*Handler) SetArgOptions

func (handler *Handler) SetArgOptions(arg string, options []Option)

SetArgOptions configures a field to display as a dropdown select with the given options.

Example:

handler.SetArgOptions("country", []htmlform.Option{
    {Label: "United States", Value: "US"},
    {Label: "Canada", Value: "CA"},
    {Label: "Mexico", Value: "MX"},
})

Panics if arg is not the name of an argument of the wrapped function, so that a typo or renamed argument doesn't silently remove the setting.

func (*Handler) SetArgRequired

func (handler *Handler) SetArgRequired(arg string, required bool)

SetArgRequired overrides the automatic required field detection. By default, arguments are required unless their type is string, a pointer, or implements IsNull() bool. Required fields are enforced by browsers with the required attribute of the form field, bool checkboxes are never marked as required.

Example:

handler.SetArgRequired("email", true)   // Make required
handler.SetArgRequired("age", false)    // Make optional

Panics if arg is not the name of an argument of the wrapped function, so that a typo or renamed argument doesn't silently remove the setting.

func (*Handler) SetArgValidator

func (handler *Handler) SetArgValidator(arg string, validator ArgValidator)

SetArgValidator registers a custom validator for a specific function argument. The validator is called server-side when the form is submitted with the context of the request and the argument value parsed from the form, which is the value the function will be called with.

If a validator returns an error, then the wrapped function is not called and a 400 Bad Request error naming the argument is handled like an error returned by the function: it is passed to the ResultsWriter and then to httpfun.HandleError if the writer returns it. The text of the validator error is shown to the client in that response, so it must not contain internal details.

Example:

handler.SetArgValidator("email", func(ctx context.Context, value any) error {
    email, _ := value.(string)
    if !strings.Contains(email, "@") {
        return errors.New("invalid email")
    }
    return nil
})

A nil validator removes the validator of the argument.

Panics if arg is not the name of an argument of the wrapped function, so that a typo or renamed argument doesn't silently remove the setting.

func (*Handler) SetCrossOriginProtection

func (handler *Handler) SetCrossOriginProtection(protection *http.CrossOriginProtection)

SetCrossOriginProtection sets the protection of form submissions against cross-site request forgery (CSRF). POST requests rejected by protection.Check are answered with 403 Forbidden without calling the function.

By default every Handler uses its own http.NewCrossOriginProtection(), which rejects POST requests that browsers send from other origins as indicated by the Sec-Fetch-Site or Origin request headers, and accepts requests without these headers like those of non-browser clients. Use http.CrossOriginProtection.AddTrustedOrigin of a custom protection to accept form submissions from other origins.

Passing nil disables the protection, for example for a handler that is only called by non-browser clients and protected otherwise, like with credentials that browsers don't send automatically.

func (*Handler) SetMaxRequestBodySize

func (handler *Handler) SetMaxRequestBodySize(size int64)

SetMaxRequestBodySize sets the maximum size in bytes of a POST request body including uploaded files. Larger requests are answered with 413 Request Entity Too Large without calling the function. Default: 100 MiB.

A size <= 0 does not disable the limit but rejects every form submission, like http.MaxBytesReader does. Pass math.MaxInt64 for no practical limit.

Uploaded files larger than httpfun.MaxMultipartFormMemory are buffered in temporary files instead of memory.

func (*Handler) SetSubmitButtonText

func (handler *Handler) SetSubmitButtonText(text string)

SetSubmitButtonText customizes the text displayed on the form's submit button. Default is "Submit".

Example:

handler.SetSubmitButtonText("Create Account")
handler.SetSubmitButtonText("Save Changes")

type Option

type Option struct {
	Label string
	Value any
}

Option represents a choice in a select dropdown field. The Label is displayed to the user, while Value is submitted with the form.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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