Forms
Overview
Forms handle user input data structures and their transformation to domain entities. The framework provides a comprehensive form handling system with validation, binding, and transformation capabilities.
Form Structures
Basic Form Definition
// src/dat/forms/create_garden_form.go
package forms
type CreateGardenForm struct {
Name string `form:"name" json:"name" validate:"required,max=100"`
Description string `form:"description" json:"description" validate:"max=500"`
Location string `form:"location" json:"location"`
Active bool `form:"active" json:"active"`
}
Update Form with ID
type UpdateGardenForm struct {
Id string `form:"id" json:"id" validate:"required"`
Name string `form:"name" json:"name" validate:"required,max=100"`
Description string `form:"description" json:"description" validate:"max=500"`
Location string `form:"location" json:"location"`
Active bool `form:"active" json:"active"`
}
Complex Form with Relationships
type CreatePlantForm struct {
Name string `form:"name" json:"name" validate:"required"`
Species string `form:"species" json:"species" validate:"required"`
GardenId string `form:"garden_id" json:"garden_id" validate:"required"`
Height float64 `form:"height" json:"height" validate:"min=0"`
Edible bool `form:"edible" json:"edible"`
PlantedAt string `form:"planted_at" json:"planted_at"` // Date as string
}
Form Binding
HTTP Request Binding
// src/srv/binders/garden_binder.go
type GardenBinder struct {
Validator *validator.Validator
}
func (this *GardenBinder) BindCreateForm(c *gin.Context, form *forms.CreateGardenForm) error {
// Bind form data from request
if err := c.ShouldBind(form); err != nil {
return fmt.Errorf("failed to bind form: %w", err)
}
// Validate form data
if err := this.Validator.Validate(form); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
return nil
}
func (this *GardenBinder) BindUpdateForm(c *gin.Context, form *forms.UpdateGardenForm) error {
// Extract ID from URL parameter
form.Id = c.Param("id")
// Bind remaining form data
if err := c.ShouldBind(form); err != nil {
return fmt.Errorf("failed to bind form: %w", err)
}
return this.Validator.Validate(form)
}
Form Validation
Validation Rules
// Using struct tags for validation
type CreateUserForm struct {
Email string `form:"email" validate:"required,email"`
Name string `form:"name" validate:"required,min=2,max=50"`
Password string `form:"password" validate:"required,min=8"`
ConfirmPassword string `form:"confirm_password" validate:"required"`
Age int `form:"age" validate:"min=13,max=120"`
}
Custom Validation
func (this *GardenBinder) BindCreateForm(c *gin.Context, form *forms.CreateGardenForm) error {
if err := c.ShouldBind(form); err != nil {
return err
}
// Custom validation logic
if err := this.validateGardenForm(form); err != nil {
return err
}
return nil
}
func (this *GardenBinder) validateGardenForm(form *forms.CreateGardenForm) error {
if strings.TrimSpace(form.Name) == "" {
return errors.New("garden name cannot be empty or just spaces")
}
// Check for inappropriate content
if strings.Contains(strings.ToLower(form.Name), "test") {
return errors.New("garden name cannot contain 'test'")
}
return nil
}
Form Transformation (Molders)
Form to Entity Transformation
// src/srv/molders/create_garden_form_molder.go
type CreateGardenFormMolder struct{}
func (this *CreateGardenFormMolder) ToEntity(form *forms.CreateGardenForm, entity *mdl.Garden) (*mdl.Garden, []string) {
var changes []string
// Direct field mapping
entity.Name = form.Name
entity.Description = form.Description
entity.Location = form.Location
entity.Active = form.Active
// Set defaults or computed values
if entity.Id == "" {
entity.Id = generateId()
changes = append(changes, "Id")
}
return entity, changes
}
func (this *CreateGardenFormMolder) ToForm(form *forms.CreateGardenForm, entity *mdl.Garden) *forms.CreateGardenForm {
form.Name = entity.Name
form.Description = entity.Description
form.Location = entity.Location
form.Active = entity.Active
return form
}
Update Form with Change Detection
func (this *UpdateGardenFormMolder) ToEntity(form *forms.UpdateGardenForm, entity *mdl.Garden) (*mdl.Garden, []string) {
var changes []string
// Track changes for optimized updates
if entity.Name != form.Name {
entity.Name = form.Name
changes = append(changes, "Name")
}
if entity.Description != form.Description {
entity.Description = form.Description
changes = append(changes, "Description")
}
if entity.Location != form.Location {
entity.Location = form.Location
changes = append(changes, "Location")
}
if entity.Active != form.Active {
entity.Active = form.Active
changes = append(changes, "Active")
}
return entity, changes
}
Form Presentation
HTML Form Generation
<!-- templates/gardens/new.html -->
<form method="POST" action="/gardens" class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">
Garden Name *
</label>
<input type="text"
name="name"
id="name"
value="{{.form.Name}}"
required
maxlength="100"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">
Description
</label>
<textarea name="description"
id="description"
rows="3"
maxlength="500"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{.form.Description}}</textarea>
</div>
<div class="flex items-center">
<input type="checkbox"
name="active"
id="active"
value="true"
{{if .form.Active}}checked{{end}}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="active" class="ml-2 block text-sm text-gray-900">
Active Garden
</label>
</div>
<div>
<button type="submit"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Create Garden
</button>
</div>
</form>
Error Handling
Validation Error Display
func (this *GardenController) Create(c *gin.Context) {
form := &forms.CreateGardenForm{}
if err := this.GardenBinder.BindCreateForm(c, form); err != nil {
// Return form with errors for re-display
c.HTML(400, "gardens/new.html", gin.H{
"title": "New Garden",
"form": form,
"error": err.Error(),
})
return
}
// Process valid form...
}
Error Messages in Templates
<!-- Display validation errors -->
{{if .error}}
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
<p class="text-red-800">{{.error}}</p>
</div>
{{end}}
<!-- Field-specific error styling -->
<input type="text"
name="name"
value="{{.form.Name}}"
class="{{if .errors.name}}border-red-500{{else}}border-gray-300{{end}} block w-full rounded-md shadow-sm">
{{if .errors.name}}
<p class="mt-1 text-sm text-red-600">{{.errors.name}}</p>
{{end}}
Advanced Form Patterns
Multi-Step Forms
type CreateGardenStepOneForm struct {
Name string `form:"name" validate:"required"`
Description string `form:"description"`
Step int `form:"step"` // Track current step
}
type CreateGardenStepTwoForm struct {
Location string `form:"location"`
Plants []string `form:"plants"`
Step int `form:"step"`
}
Dynamic Form Fields
type CreatePlantWithVarietiesForm struct {
Name string `form:"name"`
Species string `form:"species"`
Varieties []PlantVarietyFormField `form:"varieties"`
}
type PlantVarietyFormField struct {
Name string `form:"name"`
Description string `form:"description"`
Remove bool `form:"remove"` // Mark for removal
}
File Upload Forms
type UpdateGardenWithPhotoForm struct {
Id string `form:"id"`
Name string `form:"name"`
Photo *multipart.FileHeader `form:"photo"`
RemovePhoto bool `form:"remove_photo"`
}
func (this *GardenBinder) BindPhotoForm(c *gin.Context, form *UpdateGardenWithPhotoForm) error {
form.Id = c.Param("id")
if err := c.ShouldBind(form); err != nil {
return err
}
// Validate uploaded file
if form.Photo != nil {
if form.Photo.Size > 5*1024*1024 { // 5MB limit
return errors.New("photo file too large (max 5MB)")
}
contentType := form.Photo.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "image/") {
return errors.New("file must be an image")
}
}
return nil
}
Form Testing
Form Binding Tests
func TestGardenBinder_BindCreateForm(t *testing.T) {
// Setup
binder := &GardenBinder{Validator: setupValidator()}
// Create mock request
body := "name=Test Garden&description=A test&active=true"
req, _ := http.NewRequest("POST", "/", strings.NewReader(body))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
// Test binding
form := &forms.CreateGardenForm{}
err := binder.BindCreateForm(c, form)
// Assert
assert.NoError(t, err)
assert.Equal(t, "Test Garden", form.Name)
assert.Equal(t, "A test", form.Description)
assert.True(t, form.Active)
}
Form Validation Tests
func TestCreateGardenForm_Validation(t *testing.T) {
validator := setupValidator()
// Valid form
form := &forms.CreateGardenForm{
Name: "Valid Garden",
Description: "A valid description",
}
err := validator.Validate(form)
assert.NoError(t, err)
// Invalid form - empty name
form.Name = ""
err = validator.Validate(form)
assert.Error(t, err)
assert.Contains(t, err.Error(), "name")
}
Forms provide the foundation for user input handling, validation, and transformation to domain entities. They work closely with controllers, molders, and validators to ensure data integrity and user experience.