diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5bebc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/go-form diff --git a/example/address.go b/example/address.go new file mode 100644 index 0000000..61bd796 --- /dev/null +++ b/example/address.go @@ -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", + }}, + ) +} diff --git a/form/field.go b/form/field.go index 540de16..3d755f1 100644 --- a/form/field.go +++ b/form/field.go @@ -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 diff --git a/form/field_input.go b/form/field_input.go index dc7206f..3c9e2d1 100644 --- a/form/field_input.go +++ b/form/field_input.go @@ -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 +} diff --git a/form/field_textarea.go b/form/field_textarea.go new file mode 100644 index 0000000..c32f704 --- /dev/null +++ b/form/field_textarea.go @@ -0,0 +1,5 @@ +package form + +func NewFieldTextarea(name string) *Field { + return NewField(name, "textarea") +} diff --git a/form/form.go b/form/form.go index 9cbf600..4405304 100644 --- a/form/form.go +++ b/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 +} diff --git a/main.go b/main.go index 71d87b2..8ce6fc5 100644 --- a/main.go +++ b/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)) } diff --git a/theme/html5.go b/theme/html5.go index 9b50f39..1d100ac 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -1,27 +1,52 @@ package theme var Html5 = map[string]string{ - "form": `
- {{ form_error .Form nil }} - {{ .Content }} -
`, - "label": ``, - "input": ``, - "sub_form": ` - {{ form_label .Field }} + "form": `
+ {{- form_error .Form nil -}} - {{ range $field := .Field.Children }} - {{ form_row $field }} - {{ end }} + {{- 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 }}`, + "label": ` + {{ if .Field.HasOption "label" }} + {{ $label := (.Field.GetOption "label").Value }} + + {{- if ne $label "" -}} + + {{- end -}} + {{- end -}} + `, + "input": ` + {{ $type := .Field.GetOption "type" }} + + `, + "textarea": ` + + `, + "sub_form": ` + {{- range $field := .Field.Children -}} + {{- form_row $field -}} + {{- end -}} + `, + "error": ` + {{- if gt (len .Errors) 0 -}} + + {{- end -}} `, - "error": `
- {{ range $error := .Errors }} - {{ $error }}
- {{ end }} -
`, "row": `
- {{ form_label .Field }} - {{ form_error nil .Field }} - {{ form_widget .Field }} + {{- form_label .Field -}} + {{- form_error nil .Field -}} + {{- form_widget .Field -}}
`, } diff --git a/theme/renderer.go b/theme/renderer.go index a0fc09c..6a89a5a 100644 --- a/theme/renderer.go +++ b/theme/renderer.go @@ -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 { diff --git a/validation/notblank.go b/validation/notblank.go index 250ea31..e14ede7 100644 --- a/validation/notblank.go +++ b/validation/notblank.go @@ -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 {