feat: improve form rendering
This commit is contained in:
parent
d735f6e472
commit
3894fb31e9
10 changed files with 302 additions and 91 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/go-form
|
||||
48
example/address.go
Normal file
48
example/address.go
Normal 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",
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
5
form/field_textarea.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package form
|
||||
|
||||
func NewFieldTextarea(name string) *Field {
|
||||
return NewField(name, "textarea")
|
||||
}
|
||||
83
form/form.go
83
form/form.go
|
|
@ -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
77
main.go
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue