Views

Forms

Build model-aware form partials, field helpers, and REST method overrides.

By Guillermo Alvarez - Published - Updated

Form partials

GoLazy forms are intentionally partial-based. A page view calls form_for:

{{ form_for .Car . }}

For a Car model, GoLazy renders the form wrapper and then renders:

_car_form.html.tpl

Multiword model keys use underscored partial filenames, so RaceCar renders _race_car_form.html.tpl.

The partial receives the original view data plus form-specific values:

{{ text_field "Model" }}
{{ date_field "BuiltAt" }}
{{ checkbox_field "Active" }}
{{ submit_button "Save" }}

The active form is also available as .Form, and the model is available as .Model, .FormObject, and .FormData.

Field names

Field helpers ask golazy.dev/lazyschema for generated names and ids. This keeps rendering and decoding aligned.

For a model field path:

Phone.Label
Phones.0.Number
URLValue

GoLazy generates:

name="phone_label"
name="phones_0_number"
name="urlValue"

Ids include the model key as a prefix:

id="car_phone_label"

Use schema:"custom_name" on a struct field when the generated name needs an explicit override. Use schema:"-" or form:"-" to skip a field.

Decode requests

Controllers can decode the current request form directly:

var car Car
if err := c.Decode(&car); err != nil {
    return err
}

Decode parses the request form, uses lazyschema, and wraps decode failures as bad-request HTTP errors. lazyschema uses bounded defaults for key count, value count, key length, nesting depth, and slice size. Register converters for custom scalar types when the default decoder is used directly. time.Time accepts common HTML date, time, and datetime-local values by default.

For action forms, prefer a typed form generator so the action receives the decoded value:

// password_form.go
type PasswordForm struct {
    Username string
    Password string
}

func (c *SessionsController) GenPasswordForm() (*PasswordForm, error) {
    form := &PasswordForm{}
    if err := c.Decode(form); err != nil {
        return nil, err
    }
    return form, nil
}

func (c *SessionsController) Create(form *PasswordForm) error {
    username := strings.TrimSpace(form.Username)
    if err := c.sessions.SignIn(username, form.Password); err != nil {
        return err
    }
    return c.RedirectToRoute(
        "admin",
        lazycontroller.RedirectStatus(http.StatusSeeOther),
    )
}

Keep the form struct in its own file near the controller, such as password_form.go, when it is request input rather than a persisted model. Decode errors returned by GenPasswordForm use the same path as action errors: GoLazy does not call Create, and the error reaches the controller's HandleError(http.ResponseWriter, *http.Request, error) method when one is defined.

Keep application validation in the action. On validation failure, set http.StatusUnprocessableEntity, repopulate the view data, and render the form view. On success, flash if needed and redirect:

type CarForm struct {
    Model string `validate:"presence;min:3;max:80"`
    Year  string `validate:"presence"`
}

func (c *CarsController) Create(form *CarForm) error {
    if err := lazyerrors.Validator(form); err != nil {
        c.Status(http.StatusUnprocessableEntity)
        c.Set("form", form)
        c.Set("car_errors", err)
        return c.Render("new")
    }
    car, err := c.cars.Create(form.Model, form.Year)
    if err != nil {
        return err
    }
    if err := c.FlashSet("notice", "Car created"); err != nil {
        return err
    }
    return c.RedirectToRoute(
        "car",
        car.ID,
        lazycontroller.RedirectStatus(http.StatusSeeOther),
    )
}

lazyerrors.Validator returns a normal joined error. Templates can group those typed validation leaves by field:

{{ range field_errors_for .car_errors "model" }}
  <p>{{.}}</p>
{{ end }}

Use errors_for .car_errors when a template wants the full map keyed by field name.

Resource targets

Connect a model type to a REST resource:

router.Resources(cars.New, func(r *lazyroutes.Resource) {
    r.Model(Car{})
})

For new records, form_for targets the create route with POST. If the model implements:

type Resource interface {
    Persisted() bool
    RouteParam() string
}

persisted records target the update route with POST and a hidden _method=patch field.

Use option helpers when a form needs a manual target or HTML attributes:

{{ form_for .Car . (form_route "garage_car" .Car.Slug) }}
{{ form_for .Car . (form_action "/cars") (form_add_class "compact") }}
{{ form_for .Car . (form_scope "vehicle") (form_file "vehicle_fields") }}

Delete buttons

Use delete_button_for for a small delete form:

{{ delete_button_for .Car "Delete" }}

It renders a POST form with hidden _method=delete and targets the resource delete route.

Method override

HTML forms only submit GET and POST. GoLazy installs route-scoped method override middleware so a form can submit:

<input type="hidden" name="_method" value="patch">

Only routed POST form requests can override the method. The middleware accepts put, patch, and delete, skips upgrade requests, reads only a bounded prefix of the body, and replays that body for the controller.