Documentation

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.