This commit is contained in:
Simon Vieille 2025-07-16 16:43:26 +02:00
commit d735f6e472
Signed by: deblan
GPG key ID: 579388D585F70417
14 changed files with 608 additions and 0 deletions

172
form/field.go Normal file
View file

@ -0,0 +1,172 @@
package form
import (
"gitnet.fr/deblan/go-form/util"
"gitnet.fr/deblan/go-form/validation"
)
func FieldValidation(f *Field) bool {
if len(f.Children) > 0 {
isValid := true
for _, c := range f.Children {
isChildValid, errs := validation.Validate(c.Data, c.Constraints)
if len(errs) > 0 {
c.Errors = errs
}
isValid = isValid && isChildValid
}
return isValid
} else {
isValid, errs := validation.Validate(f.Data, f.Constraints)
f.Errors = []validation.Error{}
if len(errs) > 0 {
f.Errors = errs
}
return isValid
}
}
type Field struct {
Name string
Widget string
Data any
Options []*Option
Children []*Field
Constraints []validation.Constraint
Errors []validation.Error
PrepareView func() map[string]any
BeforeBind func(data any) (any, error)
Validate func(f *Field) bool
}
func NewField(name, widget string) *Field {
f := &Field{
Name: name,
Widget: widget,
Data: nil,
}
f.PrepareView = func() map[string]any {
m := make(map[string]any)
return m
}
f.BeforeBind = func(data any) (any, error) {
return data, nil
}
f.Validate = FieldValidation
return f
}
func (f *Field) HasOption(name string) bool {
for _, option := range f.Options {
if option.Name == name {
return true
}
}
return false
}
func (f *Field) GetOption(name string) *Option {
for _, option := range f.Options {
if option.Name == name {
return option
}
}
return nil
}
func (f *Field) WithOptions(options ...Option) *Field {
for _, option := range options {
if f.HasOption(option.Name) {
f.GetOption(option.Name).Value = option.Value
} else {
f.Options = append(f.Options, &option)
}
}
return f
}
func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field {
for _, constraint := range constraints {
f.Constraints = append(f.Constraints, constraint)
}
return f
}
func (f *Field) Add(children ...*Field) *Field {
for _, child := range children {
f.Children = append(f.Children, child)
}
return f
}
func (f *Field) HasChild(name string) bool {
for _, child := range f.Children {
if name == child.Name {
return true
}
}
return false
}
func (f *Field) GetChild(name string) *Field {
var result *Field
for _, child := range f.Children {
if name == child.Name {
result = child
break
}
}
return result
}
func (f *Field) Bind(data any) error {
if len(f.Children) == 0 {
f.Data = data
return nil
}
data, err := f.BeforeBind(data)
if err != nil {
return err
}
props, err := util.InspectStruct(data)
if err != nil {
return err
}
for key, value := range props {
if f.HasChild(key) {
err = f.GetChild(key).Bind(value)
if err != nil {
return err
}
}
}
return nil
}

8
form/field_input.go Normal file
View file

@ -0,0 +1,8 @@
package form
func NewFieldText(name string) *Field {
f := NewField(name, "input").
WithOptions(Option{Name: "type", Value: "text"})
return f
}

11
form/field_subform.go Normal file
View file

@ -0,0 +1,11 @@
package form
func NewFieldSubForm(name string) *Field {
f := NewField(name, "sub_form")
return f
}
func NewSubForm(name string) *Field {
return NewFieldSubForm(name)
}

117
form/form.go Normal file
View file

@ -0,0 +1,117 @@
package form
import (
"net/http"
"gitnet.fr/deblan/go-form/util"
"gitnet.fr/deblan/go-form/validation"
)
type Form struct {
Fields []*Field
Errors []validation.Error
Method string
Action string
Name string
Options []Option
}
func NewForm(fields ...*Field) *Form {
f := new(Form)
f.Method = "POST"
f.Add(fields...)
return f
}
func (f *Form) Add(fields ...*Field) {
for _, field := range fields {
f.Fields = append(f.Fields, field)
}
}
func (f *Form) HasField(name string) bool {
for _, field := range f.Fields {
if name == field.Name {
return true
}
}
return false
}
func (f *Form) GetField(name string) *Field {
var result *Field
for _, field := range f.Fields {
if name == field.Name {
result = field
break
}
}
return result
}
func (f *Form) WithMethod(v string) *Form {
f.Method = v
return f
}
func (f *Form) WithName(v string) *Form {
f.Name = v
return f
}
func (f *Form) WithAction(v string) *Form {
f.Action = v
return f
}
func (f *Form) WithOptions(options ...Option) *Form {
for _, option := range options {
f.Options = append(f.Options, option)
}
return f
}
func (f *Form) IsValid() bool {
isValid := true
for _, field := range f.Fields {
fieldIsValid := field.Validate(field)
isValid = isValid && fieldIsValid
}
return isValid
}
func (f *Form) Bind(data any) error {
props, err := util.InspectStruct(data)
if err != nil {
return err
}
for key, value := range props {
if f.HasField(key) {
err = f.GetField(key).Bind(value)
if err != nil {
return err
}
}
}
return nil
}
func (f *Form) HandleRequest(req http.Request) {
if f.Method == "POST" {
// data := req.PostForm
}
}

6
form/option.go Normal file
View file

@ -0,0 +1,6 @@
package form
type Option struct {
Name string
Value any
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module gitnet.fr/deblan/go-form
go 1.23.0

77
main.go Normal file
View file

@ -0,0 +1,77 @@
package main
import (
"fmt"
"gitnet.fr/deblan/go-form/form"
"gitnet.fr/deblan/go-form/theme"
"gitnet.fr/deblan/go-form/validation"
)
func main() {
type Address struct {
Street string
City string
ZipCode uint
}
type Person struct {
Name string
Address Address
}
data := new(Person)
data.Name = ""
data.Address = Address{
Street: "rue des camélias",
City: "",
ZipCode: 39700,
}
f := form.NewForm(
form.NewFieldText("Name").
WithOptions(
form.Option{Name: "required", Value: true},
).
WithConstraints(
validation.NotBlank{},
),
form.NewSubForm("Address").
Add(
form.NewFieldText("Street"),
form.NewFieldText("City").
WithConstraints(
validation.NotBlank{},
),
form.NewFieldText("ZipCode"),
),
).WithMethod("POST").WithAction("")
f.Bind(data)
fmt.Printf("%+v\n", f.IsValid())
render := theme.NewRenderer(theme.Html5)
v := render.RenderForm(f)
fmt.Print(v)
// r, e := theme.RenderForm(f, theme.Html5)
// fmt.Printf("%+v\n", e)
// fmt.Printf("%+v\n", r)
// fmt.Printf("%+v\n", e)
//
// fmt.Printf("%+v\n", f)
//
// for _, field := range f.Fields {
// fmt.Printf("%+v\n", *field)
//
// if len(field.Children) > 0 {
// for _, c := range field.Children {
// fmt.Printf("C %+v\n", *c)
// }
// }
// }
}

27
theme/html5.go Normal file
View file

@ -0,0 +1,27 @@
package theme
var Html5 = map[string]string{
"form": `<form action="" method="">
{{ form_error .Form nil }}
{{ .Content }}
</form>`,
"label": `<label for="">Label</label>`,
"input": `<input name="{{ .Field.Name }}" value="{{ .Field.Data }}" type="text">`,
"sub_form": `
{{ form_label .Field }}
{{ range $field := .Field.Children }}
{{ form_row $field }}
{{ end }}
`,
"error": `<div class="error">
{{ range $error := .Errors }}
{{ $error }}<br>
{{ end }}
</div>`,
"row": `<div class="row">
{{ form_label .Field }}
{{ form_error nil .Field }}
{{ form_widget .Field }}
</div>`,
}

90
theme/renderer.go Normal file
View file

@ -0,0 +1,90 @@
package theme
import (
"bytes"
"html/template"
"gitnet.fr/deblan/go-form/form"
"gitnet.fr/deblan/go-form/validation"
)
type Renderer struct {
Theme map[string]string
}
func NewRenderer(theme map[string]string) *Renderer {
r := new(Renderer)
r.Theme = theme
return r
}
func (r *Renderer) RenderForm(form *form.Form) template.HTML {
content := ""
for _, field := range form.Fields {
content = content + string(r.RenderRow(field))
}
return r.Render("form", r.Theme["form"], map[string]any{
"Form": form,
"Content": template.HTML(content),
})
}
func (r *Renderer) RenderRow(field *form.Field) template.HTML {
return r.Render("row", r.Theme["row"], map[string]any{
"Field": field,
})
}
func (r *Renderer) RenderLabel(field *form.Field) template.HTML {
return r.Render("label", r.Theme["label"], map[string]any{
"Field": field,
})
}
func (r *Renderer) RenderWidget(field *form.Field) template.HTML {
return r.Render("widget", r.Theme[field.Widget], map[string]any{
"Field": field,
})
}
func (r *Renderer) RenderError(form *form.Form, field *form.Field) template.HTML {
var errors []validation.Error
if field != nil {
errors = field.Errors
}
if form != nil {
errors = form.Errors
}
return r.Render("error", r.Theme["error"], map[string]any{
"Errors": errors,
})
}
func (r *Renderer) Render(name, tpl string, args any) template.HTML {
t, err := template.New(name).Funcs(template.FuncMap{
"form": r.RenderForm,
"form_row": r.RenderRow,
"form_label": r.RenderLabel,
"form_widget": r.RenderWidget,
"form_error": r.RenderError,
}).Parse(tpl)
if err != nil {
return template.HTML(err.Error())
}
var buf bytes.Buffer
err = t.Execute(&buf, args)
if err != nil {
return template.HTML(err.Error())
}
return template.HTML(buf.String())
}

30
util/inspect.go Normal file
View file

@ -0,0 +1,30 @@
package util
import (
"errors"
"reflect"
)
func InspectStruct(input interface{}) (map[string]interface{}, error) {
val := reflect.ValueOf(input)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil, errors.New("Invalid type")
}
result := make(map[string]interface{})
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
value := val.Field(i)
result[field.Name] = value.Interface()
}
return result, nil
}

5
validation/constraint.go Normal file
View file

@ -0,0 +1,5 @@
package validation
type Constraint interface {
Validate(data any) []Error
}

3
validation/error.go Normal file
View file

@ -0,0 +1,3 @@
package validation
type Error string

46
validation/notblank.go Normal file
View file

@ -0,0 +1,46 @@
package validation
import (
"fmt"
"reflect"
)
type NotBlank struct {
}
func (c NotBlank) Validate(data any) []Error {
isValid := true
errors := []Error{}
fmt.Printf("%+v\n", data)
t := reflect.TypeOf(data)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if data == nil {
isValid = false
} else if t.Kind() == reflect.Bool {
if data == false {
isValid = false
}
} else if t.Kind() == reflect.Array {
if len(data.([]interface{})) == 0 {
isValid = false
}
} else if t.Kind() == reflect.String {
if len(data.(string)) == 0 {
isValid = false
}
} else {
fmt.Printf("d=%+v\n", data)
}
if !isValid {
errors = append(errors, Error("This value should be blank"))
}
return errors
}

13
validation/validation.go Normal file
View file

@ -0,0 +1,13 @@
package validation
func Validate(data any, constraints []Constraint) (bool, []Error) {
errs := []Error{}
for _, constraint := range constraints {
for _, e := range constraint.Validate(data) {
errs = append(errs, e)
}
}
return len(errs) == 0, errs
}