From cef8567ad3867bbe9830f943eed9fd3850867c47 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 16 Jul 2025 22:37:15 +0200 Subject: [PATCH] feat: add data binding feat: add help --- example/address.go | 21 +++++++++++++-------- form/field.go | 41 ++++++++++++++++++++++++++++++++++++----- form/field_input.go | 14 +++++++++++--- form/form.go | 23 ++++++++++++++++++----- form/option.go | 7 +++++++ go.mod | 2 ++ go.sum | 2 ++ main.go | 7 ++----- theme/html5.go | 25 +++++++++++++++---------- theme/renderer.go | 42 ++++++++++++++++++++++++++++++++++-------- 10 files changed, 140 insertions(+), 44 deletions(-) create mode 100644 go.sum diff --git a/example/address.go b/example/address.go index 61bd796..ef58cf6 100644 --- a/example/address.go +++ b/example/address.go @@ -9,27 +9,31 @@ func CreateAddressForm() *form.Form { return form.NewForm( form.NewFieldText("Name"). WithOptions( - form.Option{Name: "label", Value: "Name"}, - form.Option{Name: "required", Value: true}, + form.NewOption("label", "Name"), + form.NewOption("required", true), + form.NewOption("help", "A help!"), ). WithConstraints( validation.NotBlank{}, ), form.NewSubForm("Address"). - WithOptions(form.Option{Name: "label", Value: "Address"}). + WithOptions(form.NewOption("label", "Address")). Add( form.NewFieldTextarea("Street"). - WithOptions(form.Option{Name: "label", Value: "Street"}). + WithOptions(form.NewOption("label", "Street")). WithConstraints( validation.NotBlank{}, ), form.NewFieldText("City"). - WithOptions(form.Option{Name: "label", Value: "City"}). + WithOptions(form.NewOption("label", "City")). WithConstraints( validation.NotBlank{}, ), form.NewFieldNumber("ZipCode"). - WithOptions(form.Option{Name: "label", Value: "Zip code"}). + WithOptions( + form.NewOption("label", "Zip code"), + form.NewOption("help", "A field help"), + ). WithConstraints( validation.NotBlank{}, ), @@ -41,8 +45,9 @@ func CreateAddressForm() *form.Form { // WithMethod("GET"). WithAction("/"). WithOptions( - form.Option{Name: "attr", Value: map[string]string{ + form.NewOption("attr", map[string]string{ "id": "my-form", - }}, + }), + form.NewOption("help", "A form help!"), ) } diff --git a/form/field.go b/form/field.go index 3d755f1..b96980e 100644 --- a/form/field.go +++ b/form/field.go @@ -44,6 +44,7 @@ type Field struct { Constraints []validation.Constraint Errors []validation.Error PrepareView func() map[string]any + BeforeMount func(data any) (any, error) BeforeBind func(data any) (any, error) Validate func(f *Field) bool Form *Form @@ -63,6 +64,10 @@ func NewField(name, widget string) *Field { return m } + f.BeforeMount = func(data any) (any, error) { + return data, nil + } + f.BeforeBind = func(data any) (any, error) { return data, nil } @@ -92,12 +97,12 @@ func (f *Field) GetOption(name string) *Option { return nil } -func (f *Field) WithOptions(options ...Option) *Field { +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) + f.Options = append(f.Options, option) } } @@ -168,14 +173,14 @@ func (f *Field) GetId() string { return name } -func (f *Field) Bind(data any) error { +func (f *Field) Mount(data any) error { if len(f.Children) == 0 { f.Data = data return nil } - data, err := f.BeforeBind(data) + data, err := f.BeforeMount(data) if err != nil { return err @@ -189,7 +194,7 @@ func (f *Field) Bind(data any) error { for key, value := range props { if f.HasChild(key) { - err = f.GetChild(key).Bind(value) + err = f.GetChild(key).Mount(value) if err != nil { return err @@ -199,3 +204,29 @@ func (f *Field) Bind(data any) error { return nil } + +func (f *Field) Bind(data map[string]any, key *string) error { + if len(f.Children) == 0 { + v, err := f.BeforeBind(f.Data) + + if err != nil { + return err + } + + if key != nil { + data[*key] = v + } else { + data[f.Name] = v + } + + return nil + } + + data[f.Name] = make(map[string]any) + + for _, child := range f.Children { + child.Bind(data[f.Name].(map[string]any), key) + } + + return nil +} diff --git a/form/field_input.go b/form/field_input.go index 3c9e2d1..2b5ee58 100644 --- a/form/field_input.go +++ b/form/field_input.go @@ -1,15 +1,23 @@ package form +import ( + "github.com/spf13/cast" +) + func NewFieldText(name string) *Field { f := NewField(name, "input"). - WithOptions(Option{Name: "type", Value: "text"}) + WithOptions(NewOption("type", "text")) return f } func NewFieldNumber(name string) *Field { f := NewField(name, "input"). - WithOptions(Option{Name: "type", Value: "number"}) + WithOptions(NewOption("type", "number")) + + f.BeforeBind = func(data any) (any, error) { + return cast.ToFloat64(data), nil + } return f } @@ -17,7 +25,7 @@ func NewFieldNumber(name string) *Field { func NewSubmit(name string) *Field { f := NewField(name, "input"). WithOptions( - Option{Name: "type", Value: "submit"}, + NewOption("type", "submit"), ) f.Data = "Submit" diff --git a/form/form.go b/form/form.go index 4405304..3c218af 100644 --- a/form/form.go +++ b/form/form.go @@ -1,6 +1,7 @@ package form import ( + "encoding/json" "net/http" "net/url" @@ -112,9 +113,9 @@ func (f *Form) WithAction(v string) *Form { return f } -func (f *Form) WithOptions(options ...Option) *Form { +func (f *Form) WithOptions(options ...*Option) *Form { for _, option := range options { - f.Options = append(f.Options, &option) + f.Options = append(f.Options, option) } return f @@ -131,7 +132,7 @@ func (f *Form) IsValid() bool { return isValid } -func (f *Form) Bind(data any) error { +func (f *Form) Mount(data any) error { props, err := util.InspectStruct(data) if err != nil { @@ -140,7 +141,7 @@ func (f *Form) Bind(data any) error { for key, value := range props { if f.HasField(key) { - err = f.GetField(key).Bind(value) + err = f.GetField(key).Mount(value) if err != nil { return err @@ -151,6 +152,18 @@ func (f *Form) Bind(data any) error { return nil } +func (f *Form) Bind(data any) error { + toBind := make(map[string]any) + + for _, field := range f.Fields { + field.Bind(toBind, nil) + } + + j, _ := json.Marshal(toBind) + + return json.Unmarshal(j, data) +} + func (f *Form) HandleRequest(req *http.Request) { var data url.Values @@ -166,7 +179,7 @@ func (f *Form) HandleRequest(req *http.Request) { for _, c := range f.GlobalFields { if data.Has(c.GetName()) { isSubmitted = true - c.Bind(data.Get(c.GetName())) + c.Mount(data.Get(c.GetName())) } } diff --git a/form/option.go b/form/option.go index c811db1..db9b86a 100644 --- a/form/option.go +++ b/form/option.go @@ -4,3 +4,10 @@ type Option struct { Name string Value any } + +func NewOption(name string, value any) *Option { + return &Option{ + Name: name, + Value: value, + } +} diff --git a/go.mod b/go.mod index f28e114..4377854 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module gitnet.fr/deblan/go-form go 1.23.0 + +require github.com/spf13/cast v1.9.2 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a02d85d --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= diff --git a/main.go b/main.go index 8ce6fc5..7b70eaa 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "log" "net/http" @@ -31,15 +30,13 @@ func main() { } f := example.CreateAddressForm() - f.Bind(data) + f.Mount(data) if r.Method == f.Method { f.HandleRequest(r) if f.IsSubmitted() && f.IsValid() { - fmt.Printf("%+v\n", "OK") - } else { - fmt.Printf("%+v\n", "KO") + f.Bind(&data) } } diff --git a/theme/html5.go b/theme/html5.go index 1d100ac..4ea7d60 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -4,42 +4,46 @@ var Html5 = map[string]string{ "form": `
{{- form_error .Form nil -}} + {{- form_help .Form -}} + {{- range $field := .Form.Fields -}} {{- form_row $field -}} {{- end -}}
`, "attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`, - // "attributes": `{{ if gt (len .Attributes) 0 }} - // {{ range $key, $value := .Attributes }} - // {{ $key }}="{{ $value }}" - // {{ end }} - // {{ end }}`, + "help": ` + {{- if gt (len .Help) 0 -}} +
{{ .Help }}
+ {{- end -}} + `, "label": ` {{ if .Field.HasOption "label" }} {{ $label := (.Field.GetOption "label").Value }} {{- if ne $label "" -}} - + {{- end -}} {{- end -}} `, "input": ` {{ $type := .Field.GetOption "type" }} - + `, "textarea": ` - + `, "sub_form": ` + {{ form_widget_help .Field }} + {{- range $field := .Field.Children -}} {{- form_row $field -}} {{- end -}} `, "error": ` {{- if gt (len .Errors) 0 -}} -