feat: improve form rendering

This commit is contained in:
Simon Vieille 2025-07-16 19:04:31 +02:00
commit 3894fb31e9
Signed by: deblan
GPG key ID: 579388D585F70417
10 changed files with 302 additions and 91 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/go-form

48
example/address.go Normal file
View file

@ -0,0 +1,48 @@
package example
import (
"gitnet.fr/deblan/go-form/form"
"gitnet.fr/deblan/go-form/validation"
)
func CreateAddressForm() *form.Form {
return form.NewForm(
form.NewFieldText("Name").
WithOptions(
form.Option{Name: "label", Value: "Name"},
form.Option{Name: "required", Value: true},
).
WithConstraints(
validation.NotBlank{},
),
form.NewSubForm("Address").
WithOptions(form.Option{Name: "label", Value: "Address"}).
Add(
form.NewFieldTextarea("Street").
WithOptions(form.Option{Name: "label", Value: "Street"}).
WithConstraints(
validation.NotBlank{},
),
form.NewFieldText("City").
WithOptions(form.Option{Name: "label", Value: "City"}).
WithConstraints(
validation.NotBlank{},
),
form.NewFieldNumber("ZipCode").
WithOptions(form.Option{Name: "label", Value: "Zip code"}).
WithConstraints(
validation.NotBlank{},
),
),
form.NewSubmit("submit"),
).
End().
WithMethod("POST").
// WithMethod("GET").
WithAction("/").
WithOptions(
form.Option{Name: "attr", Value: map[string]string{
"id": "my-form",
}},
)
}

View file

@ -1,6 +1,9 @@
package form
import (
"fmt"
"strings"
"gitnet.fr/deblan/go-form/util"
"gitnet.fr/deblan/go-form/validation"
)
@ -43,6 +46,8 @@ type Field struct {
PrepareView func() map[string]any
BeforeBind func(data any) (any, error)
Validate func(f *Field) bool
Form *Form
Parent *Field
}
func NewField(name, widget string) *Field {
@ -109,6 +114,7 @@ func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field {
func (f *Field) Add(children ...*Field) *Field {
for _, child := range children {
child.Parent = f
f.Children = append(f.Children, child)
}
@ -139,6 +145,29 @@ func (f *Field) GetChild(name string) *Field {
return result
}
func (f *Field) GetName() string {
var name string
if f.Form != nil && f.Form.Name != "" {
name = fmt.Sprintf("%s[%s]", f.Form.Name, f.Name)
} else if f.Parent != nil {
name = fmt.Sprintf("%s[%s]", f.Parent.GetName(), f.Name)
} else {
name = f.Name
}
return name
}
func (f *Field) GetId() string {
name := f.GetName()
name = strings.ReplaceAll(name, "[", "-")
name = strings.ReplaceAll(name, "]", "")
name = strings.ToLower(name)
return name
}
func (f *Field) Bind(data any) error {
if len(f.Children) == 0 {
f.Data = data

View file

@ -6,3 +6,21 @@ func NewFieldText(name string) *Field {
return f
}
func NewFieldNumber(name string) *Field {
f := NewField(name, "input").
WithOptions(Option{Name: "type", Value: "number"})
return f
}
func NewSubmit(name string) *Field {
f := NewField(name, "input").
WithOptions(
Option{Name: "type", Value: "submit"},
)
f.Data = "Submit"
return f
}

5
form/field_textarea.go Normal file
View file

@ -0,0 +1,5 @@
package form
func NewFieldTextarea(name string) *Field {
return NewField(name, "textarea")
}

View file

@ -2,34 +2,75 @@ package form
import (
"net/http"
"net/url"
"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
Fields []*Field
GlobalFields []*Field
Errors []validation.Error
Method string
Action string
Name string
Options []*Option
RequestData *url.Values
}
func NewForm(fields ...*Field) *Form {
f := new(Form)
f.Method = "POST"
f.Name = "form"
f.Add(fields...)
return f
}
func (f *Form) HasOption(name string) bool {
for _, option := range f.Options {
if option.Name == name {
return true
}
}
return false
}
func (f *Form) GetOption(name string) *Option {
for _, option := range f.Options {
if option.Name == name {
return option
}
}
return nil
}
func (f *Form) Add(fields ...*Field) {
for _, field := range fields {
field.Form = f
f.Fields = append(f.Fields, field)
}
}
func (f *Form) End() *Form {
for _, c := range f.Fields {
f.AddGlobalField(c)
}
return f
}
func (f *Form) AddGlobalField(field *Field) {
f.GlobalFields = append(f.GlobalFields, field)
for _, c := range field.Children {
f.AddGlobalField(c)
}
}
func (f *Form) HasField(name string) bool {
for _, field := range f.Fields {
if name == field.Name {
@ -73,7 +114,7 @@ func (f *Form) WithAction(v string) *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
@ -110,8 +151,30 @@ func (f *Form) Bind(data any) error {
return nil
}
func (f *Form) HandleRequest(req http.Request) {
if f.Method == "POST" {
// data := req.PostForm
func (f *Form) HandleRequest(req *http.Request) {
var data url.Values
if f.Method != "GET" {
req.ParseForm()
data = req.Form
} else {
data = req.URL.Query()
}
isSubmitted := false
for _, c := range f.GlobalFields {
if data.Has(c.GetName()) {
isSubmitted = true
c.Bind(data.Get(c.GetName()))
}
}
if isSubmitted {
f.RequestData = &data
}
}
func (f *Form) IsSubmitted() bool {
return f.RequestData != nil
}

77
main.go
View file

@ -2,10 +2,11 @@ package main
import (
"fmt"
"log"
"net/http"
"gitnet.fr/deblan/go-form/form"
"gitnet.fr/deblan/go-form/example"
"gitnet.fr/deblan/go-form/theme"
"gitnet.fr/deblan/go-form/validation"
)
func main() {
@ -20,58 +21,34 @@ func main() {
Address Address
}
data := new(Person)
data.Name = ""
data.Address = Address{
Street: "rue des camélias",
City: "",
ZipCode: 39700,
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
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 := example.CreateAddressForm()
f.Bind(data)
f.Bind(data)
if r.Method == f.Method {
f.HandleRequest(r)
fmt.Printf("%+v\n", f.IsValid())
if f.IsSubmitted() && f.IsValid() {
fmt.Printf("%+v\n", "OK")
} else {
fmt.Printf("%+v\n", "KO")
}
}
render := theme.NewRenderer(theme.Html5)
v := render.RenderForm(f)
render := theme.NewRenderer(theme.Html5)
v := render.RenderForm(f)
fmt.Print(v)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(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)
// }
// }
// }
log.Fatal(http.ListenAndServe(":1122", nil))
}

View file

@ -1,27 +1,52 @@
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 }}
"form": `<form action="{{ .Form.Action }}" method="{{ .Form.Method }}" {{ form_attr .Form }}>
{{- form_error .Form nil -}}
{{ range $field := .Field.Children }}
{{ form_row $field }}
{{ end }}
{{- range $field := .Form.Fields -}}
{{- form_row $field -}}
{{- end -}}
</form>`,
"attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`,
// "attributes": `{{ if gt (len .Attributes) 0 }}
// {{ range $key, $value := .Attributes }}
// {{ $key }}="{{ $value }}"
// {{ end }}
// {{ end }}`,
"label": `
{{ if .Field.HasOption "label" }}
{{ $label := (.Field.GetOption "label").Value }}
{{- if ne $label "" -}}
<label for="{{ .Field.GetId }}" {{ label_attr .Field }}>{{ $label }}</label>
{{- end -}}
{{- end -}}
`,
"input": `
{{ $type := .Field.GetOption "type" }}
<input id="{{ .Field.GetId }}" {{ if .Field.HasOption "required" }}{{ if (.Field.GetOption "required").Value }}required="required"{{ end }}{{ end }} name="{{ .Field.GetName }}" value="{{ .Field.Data }}" type="{{ $type.Value }}" {{ widget_attr .Field }}>
`,
"textarea": `
<textarea id="{{ .Field.GetId }}" {{ if .Field.HasOption "required" }}{{ if (.Field.GetOption "required").Value }}required="required"{{ end }}{{ end }} name="{{ .Field.GetName }}" {{ widget_attr .Field }}>{{ .Field.Data }}</textarea>
`,
"sub_form": `
{{- range $field := .Field.Children -}}
{{- form_row $field -}}
{{- end -}}
`,
"error": `
{{- if gt (len .Errors) 0 -}}
<ul class="error">
{{- range $error := .Errors -}}
<li>{{- $error -}}</li>
{{- end -}}
</ul>
{{- 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 }}
{{- form_label .Field -}}
{{- form_error nil .Field -}}
{{- form_widget .Field -}}
</div>`,
}

View file

@ -20,15 +20,8 @@ func NewRenderer(theme map[string]string) *Renderer {
}
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),
"Form": form,
})
}
@ -66,6 +59,59 @@ func (r *Renderer) RenderError(form *form.Form, field *form.Field) template.HTML
})
}
func (r *Renderer) RenderLabelAttr(field *form.Field) template.HTMLAttr {
var attributes map[string]string
if field.HasOption("label_attr") {
attributes = field.GetOption("label_attr").Value.(map[string]string)
}
return r.RenderAttr("label_attr", r.Theme["attributes"], map[string]any{
"Attributes": attributes,
})
}
func (r *Renderer) RenderWidgetAttr(field *form.Field) template.HTMLAttr {
var attributes map[string]string
if field.HasOption("attr") {
attributes = field.GetOption("attr").Value.(map[string]string)
}
return r.RenderAttr("widget_attr", r.Theme["attributes"], map[string]any{
"Attributes": attributes,
})
}
func (r *Renderer) RenderFormAttr(form *form.Form) template.HTMLAttr {
var attributes map[string]string
if form.HasOption("attr") {
attributes = form.GetOption("attr").Value.(map[string]string)
}
return r.RenderAttr("form_attr", r.Theme["attributes"], map[string]any{
"Attributes": attributes,
})
}
func (r *Renderer) RenderAttr(name, tpl string, args any) template.HTMLAttr {
t, err := template.New(name).Parse(tpl)
if err != nil {
return template.HTMLAttr("")
}
var buf bytes.Buffer
err = t.Execute(&buf, args)
if err != nil {
return template.HTMLAttr("")
}
return template.HTMLAttr(buf.String())
}
func (r *Renderer) Render(name, tpl string, args any) template.HTML {
t, err := template.New(name).Funcs(template.FuncMap{
"form": r.RenderForm,
@ -73,6 +119,9 @@ func (r *Renderer) Render(name, tpl string, args any) template.HTML {
"form_label": r.RenderLabel,
"form_widget": r.RenderWidget,
"form_error": r.RenderError,
"form_attr": r.RenderFormAttr,
"widget_attr": r.RenderWidgetAttr,
"label_attr": r.RenderLabelAttr,
}).Parse(tpl)
if err != nil {

View file

@ -1,7 +1,6 @@
package validation
import (
"fmt"
"reflect"
)
@ -11,9 +10,6 @@ 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 {
@ -35,7 +31,7 @@ func (c NotBlank) Validate(data any) []Error {
isValid = false
}
} else {
fmt.Printf("d=%+v\n", data)
errors = append(errors, Error("This value can not be processed"))
}
if !isValid {