Initial commit of form submission struct.

This commit is contained in:
mikestefanello 2021-12-22 23:40:08 -05:00
parent b61077dac9
commit 57159c4fba
5 changed files with 126 additions and 21 deletions

91
controller/form.go Normal file
View file

@ -0,0 +1,91 @@
package controller
import (
"reflect"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
type FormSubmission struct {
IsSubmitted bool
Errors map[string][]string
}
func (f *FormSubmission) Process(ctx echo.Context, form interface{}) error {
f.Errors = make(map[string][]string)
f.IsSubmitted = true
// Validate the form
if err := ctx.Validate(form); err != nil {
f.setErrorMessages(form, err)
}
return nil
}
func (f FormSubmission) HasErrors() bool {
if f.Errors == nil {
return false
}
return len(f.Errors) > 0
}
func (f FormSubmission) FieldHasError(fieldName string) bool {
return len(f.GetFieldErrors(fieldName)) > 0
}
func (f FormSubmission) GetFieldErrors(fieldName string) []string {
if f.Errors == nil {
return []string{}
}
errors, has := f.Errors[fieldName]
if !has {
return []string{}
}
return errors
}
func (f *FormSubmission) setErrorMessages(form interface{}, err error) {
// Only this is supported right now
ves, ok := err.(validator.ValidationErrors)
if !ok {
return
}
formType := reflect.TypeOf(form)
for _, ve := range ves {
var message string
// Default the field form name to the name of the struct field
fieldName := ve.StructField()
// Attempt to get the form field name from the field's struct tag
if field, ok := formType.FieldByName(ve.Field()); ok {
if fieldNameTag := field.Tag.Get("form"); fieldNameTag != "" {
fieldName = fieldNameTag
}
}
// Provide better error messages depending on the failed validation tag
// This should be expanded as you use additional tags in your validation
switch ve.Tag() {
case "required":
message = "This field is required."
case "email":
message = "Enter a valid email address."
case "eqfield":
message = "Does not match."
default:
message = "Invalid value."
}
// Add the error
f.Errors[fieldName] = append(f.Errors[fieldName], message)
}
}

View file

@ -16,8 +16,9 @@ type (
} }
ContactForm struct { ContactForm struct {
Email string `form:"email" validate:"required,email" label:"Email address"` Email string `form:"email" validate:"required,email"`
Message string `form:"message" validate:"required" label:"Message"` Message string `form:"message" validate:"required"`
Submission controller.FormSubmission
} }
) )
@ -36,28 +37,31 @@ func (c *Contact) Get(ctx echo.Context) error {
} }
func (c *Contact) Post(ctx echo.Context) error { func (c *Contact) Post(ctx echo.Context) error {
fail := func(message string, err error) error { //fail := func(message string, err error) error {
ctx.Logger().Errorf("%s: %v", message, err) // ctx.Logger().Errorf("%s: %v", message, err)
msg.Danger(ctx, "An error occurred. Please try again.") // msg.Danger(ctx, "An error occurred. Please try again.")
return c.Get(ctx) // return c.Get(ctx)
} //}
// Parse the form values // Parse the form values
var form ContactForm var form ContactForm
if err := ctx.Bind(&form); err != nil { if err := ctx.Bind(&form); err != nil {
return fail("unable to parse contact form", err) ctx.Logger().Error(err)
} }
if err := form.Submission.Process(ctx, form); err != nil {
// TOOD
}
ctx.Set(context.FormKey, form) ctx.Set(context.FormKey, form)
// Validate the form if form.Submission.HasErrors() {
if err := ctx.Validate(form); err != nil {
c.SetValidationErrorMessages(ctx, err, form)
return c.Get(ctx) return c.Get(ctx)
} }
p := controller.NewHTMX(ctx) htmx := controller.NewHTMXRequest(ctx)
if p.Request.Enabled { if htmx.Enabled {
return ctx.String(http.StatusOK, "<b>HELLO!</b>") return ctx.String(http.StatusOK, "<b>HELLO!</b>")
} else { } else {
msg.Success(ctx, "Thank you for contacting us!") msg.Success(ctx, "Thank you for contacting us!")

View file

@ -1,3 +1,9 @@
{{define "csrf"}} {{define "csrf"}}
<input type="hidden" name="csrf" value="{{.CSRF}}"/> <input type="hidden" name="csrf" value="{{.CSRF}}"/>
{{end}}
{{define "form-field-errors"}}
{{range .}}
<p class="help is-danger">{{.}}</p>
{{end}}
{{end}} {{end}}

View file

@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" style="height:100%;">
<head> <head>
{{template "metatags" .}} {{template "metatags" .}}
{{template "css" .}} {{template "css" .}}
{{template "js" .}} {{template "js" .}}
</head> </head>
<body> <body class="has-background-light" style="min-height:100%;">
<nav class="navbar is-dark"> <nav class="navbar is-dark">
<div class="container" hx-boost="true"> <div class="container" hx-boost="true">
<div class="navbar-brand"> <div class="navbar-brand">
@ -28,12 +28,14 @@
<section class="section"> <section class="section">
<div class="container"> <div class="container">
{{- if .Title}} <div class="box">
<h1 class="title">{{.Title}}</h1> {{- if .Title}}
{{- end}} <h1 class="title">{{.Title}}</h1>
{{- end}}
{{template "messages" .}} {{template "messages" .}}
{{template "content" .}} {{template "content" .}}
</div>
</div> </div>
</section> </section>
</body> </body>

View file

@ -1,10 +1,11 @@
{{define "content"}} {{define "content"}}
<form id="contact" method="post" hx-post> <form id="contact" method="post" >
<div class="field"> <div class="field">
<label for="email" class="label">Email address</label> <label for="email" class="label">Email address</label>
<div class="control"> <div class="control">
<input id="email" name="email" type="email" class="input" value="{{.Data.Email}}"> <input id="email" name="email" type="email" class="input" value="{{.Data.Email}}">
</div> </div>
{{template "form-field-errors" (.Data.Submission.GetFieldErrors "email")}}
</div> </div>
<div class="field"> <div class="field">
@ -12,6 +13,7 @@
<div class="control"> <div class="control">
<textarea id="message" name="message" class="textarea">{{.Data.Message}}</textarea> <textarea id="message" name="message" class="textarea">{{.Data.Message}}</textarea>
</div> </div>
{{template "form-field-errors" (.Data.Submission.GetFieldErrors "message")}}
</div> </div>
<div class="field is-grouped"> <div class="field is-grouped">