Views
Forms
Build model-aware form partials, field helpers, and REST method overrides.
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.