diff --git a/pkg/form/form.go b/pkg/form/form.go index 1392d50..d7103ba 100644 --- a/pkg/form/form.go +++ b/pkg/form/form.go @@ -3,6 +3,7 @@ package form import ( "github.com/labstack/echo/v4" "github.com/occult/pagode/pkg/context" + "github.com/romsar/gonertia/v2" ) // Form represents a form that can be submitted and validated. @@ -30,6 +31,9 @@ type Form interface { // GetFieldErrors returns the validation errors for a given struct field. GetFieldErrors(fieldName string) []string + + // GetErrors returns all validation errors keyed by field name. + GetErrors() map[string][]string } // Get gets a form from the context or initializes a new copy if one is not set. @@ -51,3 +55,22 @@ func Clear(ctx echo.Context) { func Submit(ctx echo.Context, form Form) error { return form.Submit(ctx, form) } + +// ShareErrors shares form validation errors as Inertia props. +// It merges the errors with existing Inertia props to preserve +// previously set shared data like auth, flash, etc. +func ShareErrors(ctx echo.Context, form Form) { + errors := form.GetErrors() + if len(errors) == 0 { + return + } + + existingProps := gonertia.PropsFromContext(ctx.Request().Context()) + if existingProps == nil { + existingProps = make(map[string]any) + } + existingProps["errors"] = errors + + newCtx := gonertia.SetProps(ctx.Request().Context(), existingProps) + ctx.SetRequest(ctx.Request().WithContext(newCtx)) +} diff --git a/pkg/form/form_test.go b/pkg/form/form_test.go index 360c31f..1dc7f54 100644 --- a/pkg/form/form_test.go +++ b/pkg/form/form_test.go @@ -1,11 +1,13 @@ package form import ( + "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/occult/pagode/pkg/context" "github.com/occult/pagode/pkg/tests" + "github.com/romsar/gonertia/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -61,3 +63,109 @@ func TestGetClear(t *testing.T) { assert.Empty(t, got.Name) }) } + +func TestShareErrorsPreservesExistingProps(t *testing.T) { + e := echo.New() + req := httptest.NewRequest("POST", "/test", nil) + rec := httptest.NewRecorder() + ctx := e.NewContext(req, rec) + + // Set up existing Inertia props + existingProps := map[string]any{ + "auth": map[string]any{ + "user": map[string]any{"name": "Test User"}, + }, + "flash": map[string]any{ + "success": []string{"Welcome!"}, + }, + } + newReqCtx := gonertia.SetProps(req.Context(), existingProps) + ctx.SetRequest(req.WithContext(newReqCtx)) + + // Create a form with errors + form := &mockForm{} + form.SetFieldError("Email", "Email is required") + form.SetFieldError("Password", "Password is required") + + // Share errors + ShareErrors(ctx, form) + + // Get props from context + props := gonertia.PropsFromContext(ctx.Request().Context()) + require.NotNil(t, props) + + // Verify existing props are preserved + auth, ok := props["auth"].(map[string]any) + require.True(t, ok, "auth prop should exist") + user, ok := auth["user"].(map[string]any) + require.True(t, ok, "auth.user should exist") + assert.Equal(t, "Test User", user["name"]) + + flash, ok := props["flash"].(map[string]any) + require.True(t, ok, "flash prop should exist") + success, ok := flash["success"].([]string) + require.True(t, ok, "flash.success should exist") + assert.Equal(t, []string{"Welcome!"}, success) + + // Verify errors are added + errors, ok := props["errors"].(map[string][]string) + require.True(t, ok, "errors prop should exist") + assert.Equal(t, []string{"Email is required"}, errors["Email"]) + assert.Equal(t, []string{"Password is required"}, errors["Password"]) +} + +func TestShareErrorsWorksWithNoExistingProps(t *testing.T) { + e := echo.New() + req := httptest.NewRequest("POST", "/test", nil) + rec := httptest.NewRecorder() + ctx := e.NewContext(req, rec) + + // Create a form with errors + form := &mockForm{} + form.SetFieldError("Name", "Name is required") + + // Share errors (no existing props) + ShareErrors(ctx, form) + + // Get props from context + props := gonertia.PropsFromContext(ctx.Request().Context()) + require.NotNil(t, props) + + // Verify errors are added + errors, ok := props["errors"].(map[string][]string) + require.True(t, ok, "errors prop should exist") + assert.Equal(t, []string{"Name is required"}, errors["Name"]) +} + +func TestShareErrorsNoOpWhenNoErrors(t *testing.T) { + e := echo.New() + req := httptest.NewRequest("POST", "/test", nil) + rec := httptest.NewRecorder() + ctx := e.NewContext(req, rec) + + // Set up existing Inertia props + existingProps := map[string]any{ + "auth": map[string]any{ + "user": "test", + }, + } + newReqCtx := gonertia.SetProps(req.Context(), existingProps) + ctx.SetRequest(req.WithContext(newReqCtx)) + + // Create a form with NO errors + form := &mockForm{} + + // Share errors (should be no-op) + ShareErrors(ctx, form) + + // Get props from context + props := gonertia.PropsFromContext(ctx.Request().Context()) + require.NotNil(t, props) + + // Verify existing props are untouched + assert.Equal(t, "test", props["auth"].(map[string]any)["user"]) + + // Verify no errors key was added + _, hasErrors := props["errors"] + assert.False(t, hasErrors, "errors prop should not exist when there are no errors") +} diff --git a/pkg/form/submission.go b/pkg/form/submission.go index 435e5af..4ddeda1 100644 --- a/pkg/form/submission.go +++ b/pkg/form/submission.go @@ -73,6 +73,13 @@ func (f *Submission) GetFieldErrors(fieldName string) []string { return f.errors[fieldName] } +func (f *Submission) GetErrors() map[string][]string { + if f.errors == nil { + return make(map[string][]string) + } + return f.errors +} + // setErrorMessages sets errors messages on the submission for all fields that failed validation. func (f *Submission) setErrorMessages(err error) { // Only this is supported right now