feat: add fields (choice, checkbox, date)
feat: replace json encoding with mapstructure feat: improve notblank constraint feat: add mail contraint feat: add template for fields
This commit is contained in:
parent
cef8567ad3
commit
b7c2ddeebf
15 changed files with 702 additions and 112 deletions
|
|
@ -1,53 +0,0 @@
|
|||
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.NewOption("label", "Name"),
|
||||
form.NewOption("required", true),
|
||||
form.NewOption("help", "A help!"),
|
||||
).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewSubForm("Address").
|
||||
WithOptions(form.NewOption("label", "Address")).
|
||||
Add(
|
||||
form.NewFieldTextarea("Street").
|
||||
WithOptions(form.NewOption("label", "Street")).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewFieldText("City").
|
||||
WithOptions(form.NewOption("label", "City")).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewFieldNumber("ZipCode").
|
||||
WithOptions(
|
||||
form.NewOption("label", "Zip code"),
|
||||
form.NewOption("help", "A field help"),
|
||||
).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
),
|
||||
form.NewSubmit("submit"),
|
||||
).
|
||||
End().
|
||||
WithMethod("POST").
|
||||
// WithMethod("GET").
|
||||
WithAction("/").
|
||||
WithOptions(
|
||||
form.NewOption("attr", map[string]string{
|
||||
"id": "my-form",
|
||||
}),
|
||||
form.NewOption("help", "A form help!"),
|
||||
)
|
||||
}
|
||||
135
example/form.go
Normal file
135
example/form.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package example
|
||||
|
||||
import (
|
||||
"gitnet.fr/deblan/go-form/form"
|
||||
"gitnet.fr/deblan/go-form/validation"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
Tags []Tag
|
||||
Tags2 []Tag
|
||||
Tags3 Tag
|
||||
Tag Tag
|
||||
}
|
||||
|
||||
func CreateExampleForm2() *form.Form {
|
||||
tags := []Tag{Tag{"tag1"}, Tag{"tag2"}, Tag{"tag3"}}
|
||||
|
||||
choices := form.NewChoices(tags).
|
||||
WithLabelBuilder(func(key int, item any) string {
|
||||
return item.(Tag).Name
|
||||
})
|
||||
|
||||
return form.NewForm(
|
||||
form.NewFieldChoice("Tag").
|
||||
WithOptions(
|
||||
form.NewOption("choices", choices),
|
||||
form.NewOption("label", "Tag"),
|
||||
form.NewOption("required", true),
|
||||
).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewFieldChoice("Tags").
|
||||
WithSlice().
|
||||
WithOptions(
|
||||
form.NewOption("choices", choices),
|
||||
form.NewOption("label", "Tags"),
|
||||
form.NewOption("multiple", true),
|
||||
form.NewOption("required", true),
|
||||
).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewFieldChoice("Tags2").
|
||||
WithSlice().
|
||||
WithOptions(
|
||||
form.NewOption("choices", choices),
|
||||
form.NewOption("label", "Tags"),
|
||||
form.NewOption("multiple", true),
|
||||
form.NewOption("expanded", true),
|
||||
form.NewOption("required", true),
|
||||
).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewFieldChoice("Tag3").
|
||||
WithOptions(
|
||||
form.NewOption("choices", choices),
|
||||
form.NewOption("label", "Tag"),
|
||||
form.NewOption("expanded", true),
|
||||
),
|
||||
form.NewSubmit("submit"),
|
||||
).
|
||||
End().
|
||||
WithMethod("POST").
|
||||
WithAction("/")
|
||||
}
|
||||
|
||||
func CreateExampleForm() *form.Form {
|
||||
return form.NewForm(
|
||||
form.NewFieldText("Name").
|
||||
WithOptions(
|
||||
form.NewOption("label", "Name"),
|
||||
form.NewOption("required", true),
|
||||
form.NewOption("help", "A help!"),
|
||||
).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewFieldDate("Date").WithOptions(form.NewOption("label", "Date")),
|
||||
// form.NewFieldDatetime("DateTime").WithOptions(form.NewOption("label", "DateTime")),
|
||||
form.NewFieldDatetimeLocal("DateTime").WithOptions(form.NewOption("label", "DateTimeLocal")),
|
||||
form.NewFieldTime("Time").WithOptions(form.NewOption("label", "Time")),
|
||||
form.NewFieldCheckbox("Checkbox").WithOptions(form.NewOption("label", "Checkbox")),
|
||||
form.NewSubForm("Address").
|
||||
WithOptions(form.NewOption("label", "Address")).
|
||||
Add(
|
||||
form.NewFieldTextarea("Street").
|
||||
WithOptions(form.NewOption("label", "Street")).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewFieldText("City").
|
||||
WithOptions(form.NewOption("label", "City")).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewFieldNumber("ZipCode").
|
||||
WithOptions(
|
||||
form.NewOption("label", "Zip code"),
|
||||
form.NewOption("help", "A field help"),
|
||||
).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
),
|
||||
form.NewFieldRange("Foo").
|
||||
WithOptions(
|
||||
form.NewOption("label", "Foo"),
|
||||
),
|
||||
form.NewFieldMail("Email").
|
||||
WithOptions(
|
||||
form.NewOption("label", "Email"),
|
||||
).
|
||||
WithConstraints(
|
||||
validation.NotBlank{},
|
||||
validation.Mail{},
|
||||
),
|
||||
),
|
||||
form.NewSubmit("submit"),
|
||||
).
|
||||
End().
|
||||
WithMethod("POST").
|
||||
// WithMethod("GET").
|
||||
WithAction("/").
|
||||
WithOptions(
|
||||
form.NewOption("attr", map[string]string{
|
||||
"id": "my-form",
|
||||
}),
|
||||
form.NewOption("help", "A form help!"),
|
||||
)
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ func FieldValidation(f *Field) bool {
|
|||
isValid := true
|
||||
|
||||
for _, c := range f.Children {
|
||||
c.ResetErrors()
|
||||
isChildValid, errs := validation.Validate(c.Data, c.Constraints)
|
||||
|
||||
if len(errs) > 0 {
|
||||
|
|
@ -24,8 +25,8 @@ func FieldValidation(f *Field) bool {
|
|||
|
||||
return isValid
|
||||
} else {
|
||||
f.ResetErrors()
|
||||
isValid, errs := validation.Validate(f.Data, f.Constraints)
|
||||
f.Errors = []validation.Error{}
|
||||
|
||||
if len(errs) > 0 {
|
||||
f.Errors = errs
|
||||
|
|
@ -47,6 +48,7 @@ type Field struct {
|
|||
BeforeMount func(data any) (any, error)
|
||||
BeforeBind func(data any) (any, error)
|
||||
Validate func(f *Field) bool
|
||||
IsSlice bool
|
||||
Form *Form
|
||||
Parent *Field
|
||||
}
|
||||
|
|
@ -109,6 +111,18 @@ func (f *Field) WithOptions(options ...*Option) *Field {
|
|||
return f
|
||||
}
|
||||
|
||||
func (f *Field) ResetErrors() *Field {
|
||||
f.Errors = []validation.Error{}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *Field) WithSlice() *Field {
|
||||
f.IsSlice = true
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field {
|
||||
for _, constraint := range constraints {
|
||||
f.Constraints = append(f.Constraints, constraint)
|
||||
|
|
@ -117,6 +131,18 @@ func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field {
|
|||
return f
|
||||
}
|
||||
|
||||
func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field {
|
||||
f.BeforeMount = callback
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field {
|
||||
f.BeforeBind = callback
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *Field) Add(children ...*Field) *Field {
|
||||
for _, child := range children {
|
||||
child.Parent = f
|
||||
|
|
@ -174,18 +200,18 @@ func (f *Field) GetId() string {
|
|||
}
|
||||
|
||||
func (f *Field) Mount(data any) error {
|
||||
if len(f.Children) == 0 {
|
||||
f.Data = data
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := f.BeforeMount(data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(f.Children) == 0 {
|
||||
f.Data = data
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
props, err := util.InspectStruct(data)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
32
form/field_checkbox.go
Normal file
32
form/field_checkbox.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package form
|
||||
|
||||
import (
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func NewFieldCheckbox(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(NewOption("type", "checkbox")).
|
||||
WithBeforeMount(func(data any) (any, error) {
|
||||
switch data.(type) {
|
||||
case string:
|
||||
data = data == "1"
|
||||
case bool:
|
||||
return data, nil
|
||||
}
|
||||
|
||||
return cast.ToInt(data), nil
|
||||
}).
|
||||
WithBeforeBind(func(data any) (any, error) {
|
||||
switch data.(type) {
|
||||
case string:
|
||||
return data == "1", nil
|
||||
case bool:
|
||||
return data, nil
|
||||
}
|
||||
|
||||
return cast.ToBool(data), nil
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
137
form/field_choice.go
Normal file
137
form/field_choice.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package form
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type Choice struct {
|
||||
Value string
|
||||
Label string
|
||||
Data any
|
||||
}
|
||||
|
||||
func (c Choice) Match(value string) bool {
|
||||
return c.Value == value
|
||||
}
|
||||
|
||||
type Choices struct {
|
||||
Data any
|
||||
ValueBuilder func(key int, item any) string
|
||||
LabelBuilder func(key int, item any) string
|
||||
}
|
||||
|
||||
func (c *Choices) Match(f *Field, value string) bool {
|
||||
if f.IsSlice {
|
||||
v := reflect.ValueOf(f.Data)
|
||||
|
||||
for key, _ := range c.GetChoices() {
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i).Interface()
|
||||
|
||||
switch item.(type) {
|
||||
case string:
|
||||
if item == value {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
if c.ValueBuilder(key, item) == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return f.Data == value
|
||||
}
|
||||
|
||||
func (c *Choices) WithValueBuilder(builder func(key int, item any) string) *Choices {
|
||||
c.ValueBuilder = builder
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Choices) WithLabelBuilder(builder func(key int, item any) string) *Choices {
|
||||
c.LabelBuilder = builder
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Choices) GetChoices() []Choice {
|
||||
choices := []Choice{}
|
||||
|
||||
v := reflect.ValueOf(c.Data)
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Slice, reflect.Array, reflect.String, reflect.Map:
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
choices = append(choices, Choice{
|
||||
Value: c.ValueBuilder(i, v.Index(i).Interface()),
|
||||
Label: c.LabelBuilder(i, v.Index(i).Interface()),
|
||||
Data: v.Index(i).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return choices
|
||||
}
|
||||
|
||||
func NewChoices(items any) *Choices {
|
||||
builder := func(key int, item any) string {
|
||||
return cast.ToString(key)
|
||||
}
|
||||
|
||||
choices := Choices{
|
||||
ValueBuilder: builder,
|
||||
LabelBuilder: builder,
|
||||
Data: items,
|
||||
}
|
||||
|
||||
return &choices
|
||||
}
|
||||
|
||||
func NewFieldChoice(name string) *Field {
|
||||
f := NewField(name, "choice").
|
||||
WithOptions(
|
||||
NewOption("choices", &Choices{}),
|
||||
NewOption("expanded", false),
|
||||
NewOption("multiple", false),
|
||||
NewOption("empty_choice_label", ""),
|
||||
)
|
||||
|
||||
f.WithBeforeBind(func(data any) (any, error) {
|
||||
choices := f.GetOption("choices").Value.(*Choices)
|
||||
|
||||
switch data.(type) {
|
||||
case string:
|
||||
v := data.(string)
|
||||
for _, c := range choices.GetChoices() {
|
||||
if c.Match(v) {
|
||||
return c.Data, nil
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
v := reflect.ValueOf(data)
|
||||
var res []interface{}
|
||||
|
||||
for _, choice := range choices.GetChoices() {
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i).Interface().(string)
|
||||
if choice.Match(item) {
|
||||
res = append(res, choice.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return data, nil
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
|
|
@ -13,20 +13,41 @@ func NewFieldText(name string) *Field {
|
|||
|
||||
func NewFieldNumber(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(NewOption("type", "number"))
|
||||
WithOptions(NewOption("type", "number")).
|
||||
WithBeforeBind(func(data any) (any, error) {
|
||||
return cast.ToFloat64(data), nil
|
||||
})
|
||||
|
||||
f.BeforeBind = func(data any) (any, error) {
|
||||
return cast.ToFloat64(data), nil
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func NewFieldMail(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(NewOption("type", "email"))
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func NewFieldRange(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(NewOption("type", "range")).
|
||||
WithBeforeBind(func(data any) (any, error) {
|
||||
return cast.ToFloat64(data), nil
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func NewFieldPassword(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(NewOption("type", "password"))
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func NewSubmit(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(
|
||||
NewOption("type", "submit"),
|
||||
)
|
||||
WithOptions(NewOption("type", "submit"))
|
||||
|
||||
f.Data = "Submit"
|
||||
|
||||
|
|
|
|||
90
form/field_input_date.go
Normal file
90
form/field_input_date.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package form
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func DateBeforeMount(data any, format string) (any, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch data.(type) {
|
||||
case string:
|
||||
return data, nil
|
||||
case time.Time:
|
||||
return data.(time.Time).Format(format), nil
|
||||
case *time.Time:
|
||||
v := data.(*time.Time)
|
||||
if v != nil {
|
||||
return v.Format(format), nil
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func NewFieldDate(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(NewOption("type", "date")).
|
||||
WithBeforeMount(func(data any) (any, error) {
|
||||
return DateBeforeMount(data, "2006-01-02")
|
||||
}).
|
||||
WithBeforeBind(func(data any) (any, error) {
|
||||
return time.Parse(time.DateOnly, data.(string))
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func NewFieldDatetime(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(NewOption("type", "datetime")).
|
||||
WithBeforeMount(func(data any) (any, error) {
|
||||
return DateBeforeMount(data, "2006-01-02 15:04")
|
||||
}).
|
||||
WithBeforeBind(func(data any) (any, error) {
|
||||
return time.Parse("2006-01-02T15:04", data.(string))
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func NewFieldDatetimeLocal(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(
|
||||
NewOption("type", "datetime-local"),
|
||||
).
|
||||
WithBeforeMount(func(data any) (any, error) {
|
||||
return DateBeforeMount(data, "2006-01-02 15:04")
|
||||
}).
|
||||
WithBeforeBind(func(data any) (any, error) {
|
||||
a, b := time.Parse("2006-01-02T15:04", data.(string))
|
||||
|
||||
return a, b
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func NewFieldTime(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(NewOption("type", "time")).
|
||||
WithBeforeMount(func(data any) (any, error) {
|
||||
return DateBeforeMount(data, "15:04")
|
||||
}).
|
||||
WithBeforeBind(func(data any) (any, error) {
|
||||
if data != nil {
|
||||
v := data.(string)
|
||||
|
||||
if len(v) > 0 {
|
||||
return time.Parse(time.TimeOnly, fmt.Sprintf("%s:00", v))
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
20
form/form.go
20
form/form.go
|
|
@ -1,10 +1,10 @@
|
|||
package form
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"gitnet.fr/deblan/go-form/util"
|
||||
"gitnet.fr/deblan/go-form/validation"
|
||||
)
|
||||
|
|
@ -49,6 +49,12 @@ func (f *Form) GetOption(name string) *Option {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) ResetErrors() *Form {
|
||||
f.Errors = []validation.Error{}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *Form) Add(fields ...*Field) {
|
||||
for _, field := range fields {
|
||||
field.Form = f
|
||||
|
|
@ -123,6 +129,7 @@ func (f *Form) WithOptions(options ...*Option) *Form {
|
|||
|
||||
func (f *Form) IsValid() bool {
|
||||
isValid := true
|
||||
f.ResetErrors()
|
||||
|
||||
for _, field := range f.Fields {
|
||||
fieldIsValid := field.Validate(field)
|
||||
|
|
@ -159,9 +166,7 @@ func (f *Form) Bind(data any) error {
|
|||
field.Bind(toBind, nil)
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(toBind)
|
||||
|
||||
return json.Unmarshal(j, data)
|
||||
return mapstructure.Decode(toBind, data)
|
||||
}
|
||||
|
||||
func (f *Form) HandleRequest(req *http.Request) {
|
||||
|
|
@ -179,7 +184,12 @@ func (f *Form) HandleRequest(req *http.Request) {
|
|||
for _, c := range f.GlobalFields {
|
||||
if data.Has(c.GetName()) {
|
||||
isSubmitted = true
|
||||
c.Mount(data.Get(c.GetName()))
|
||||
|
||||
if c.IsSlice {
|
||||
c.Mount(data[c.GetName()])
|
||||
} else {
|
||||
c.Mount(data.Get(c.GetName()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
5
go.mod
5
go.mod
|
|
@ -2,4 +2,7 @@ module gitnet.fr/deblan/go-form
|
|||
|
||||
go 1.23.0
|
||||
|
||||
require github.com/spf13/cast v1.9.2 // indirect
|
||||
require (
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
)
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -1,2 +1,4 @@
|
|||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
|
|
|
|||
94
main.go
94
main.go
|
|
@ -1,8 +1,11 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitnet.fr/deblan/go-form/example"
|
||||
"gitnet.fr/deblan/go-form/theme"
|
||||
|
|
@ -15,21 +18,37 @@ func main() {
|
|||
ZipCode uint
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
type Foo struct {
|
||||
Name string
|
||||
Address Address
|
||||
Date time.Time
|
||||
DateTime time.Time
|
||||
Time time.Time
|
||||
Checkbox bool
|
||||
}
|
||||
|
||||
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,
|
||||
// now := time.Now()
|
||||
// data := new(Foo)
|
||||
// data.Name = ""
|
||||
// data.Date = now
|
||||
// data.DateTime = now
|
||||
// data.Time = now
|
||||
// data.Address = Address{
|
||||
// Street: "",
|
||||
// City: "",
|
||||
// ZipCode: 39700,
|
||||
// }
|
||||
//
|
||||
// f := example.CreateExampleForm()
|
||||
// f.Mount(data)
|
||||
|
||||
data := example.Post{
|
||||
// Tags: []example.Tag{example.Tag{"tag1"}, example.Tag{"tag2"}, example.Tag{"tag3"}},
|
||||
Tag: example.Tag{"tag1"},
|
||||
}
|
||||
|
||||
f := example.CreateAddressForm()
|
||||
f := example.CreateExampleForm2()
|
||||
f.Mount(data)
|
||||
|
||||
if r.Method == f.Method {
|
||||
|
|
@ -37,14 +56,65 @@ func main() {
|
|||
|
||||
if f.IsSubmitted() && f.IsValid() {
|
||||
f.Bind(&data)
|
||||
fmt.Printf("BIND=%+v\n", data)
|
||||
}
|
||||
}
|
||||
|
||||
render := theme.NewRenderer(theme.Html5)
|
||||
v := render.RenderForm(f)
|
||||
|
||||
tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Form</title>
|
||||
<style>
|
||||
input[type="text"],
|
||||
input[type="date"],
|
||||
input[type="datetime"],
|
||||
input[type="time"],
|
||||
input[type="range"],
|
||||
input[type="email"],
|
||||
select,
|
||||
input[type="datetime-local"],
|
||||
textarea {
|
||||
box-sizing: border-box;
|
||||
padding: 9px;
|
||||
margin: 10px 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.form-errors {
|
||||
margin: 0;
|
||||
padding: 5px 0 0 0;
|
||||
color: red;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.form-errors li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
color: blue;
|
||||
font-size: 9px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{ form .Form }}
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte(v))
|
||||
tpl.Execute(w, map[string]any{
|
||||
"Form": f,
|
||||
// "Post": data,
|
||||
})
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(":1122", nil))
|
||||
|
|
|
|||
|
|
@ -26,18 +26,70 @@ var Html5 = map[string]string{
|
|||
{{- 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 }}" {{ form_widget_attr .Field }}>
|
||||
{{- $type := .Field.GetOption "type" -}}
|
||||
{{- $checked := and (eq (.Field.GetOption "type").Value "checkbox") (.Field.Data) -}}
|
||||
{{- $required := and (.Field.HasOption "required") (.Field.GetOption "required").Value -}}
|
||||
{{- $value := .Field.Data -}}
|
||||
|
||||
{{- if eq $type.Value "checkbox" -}}
|
||||
{{- $value = 1 -}}
|
||||
{{- end -}}
|
||||
|
||||
<input id="{{ .Field.GetId }}" {{ if $checked }}checked{{ end }} {{ if $required }}required="required"{{ end }} name="{{ .Field.GetName }}" value="{{ $value }}" type="{{ $type.Value }}" {{ form_widget_attr .Field }}>
|
||||
`,
|
||||
"textarea": `
|
||||
<textarea id="{{ .Field.GetId }}" {{ if .Field.HasOption "required" }}{{ if (.Field.GetOption "required").Value }}required="required"{{ end }}{{ end }} name="{{ .Field.GetName }}" {{ form_widget_attr .Field }}>{{ .Field.Data }}</textarea>
|
||||
`,
|
||||
"sub_form": `
|
||||
{{ form_widget_help .Field }}
|
||||
"choice": `
|
||||
{{- $required := and (.Field.HasOption "required") (.Field.GetOption "required").Value -}}
|
||||
{{- $isExpanded := (.Field.GetOption "expanded").Value -}}
|
||||
{{- $isMultiple := (.Field.GetOption "multiple").Value -}}
|
||||
{{- $emptyChoiceLabel := (.Field.GetOption "empty_choice_label").Value -}}
|
||||
{{- $choices := (.Field.GetOption "choices").Value -}}
|
||||
{{- $field := .Field -}}
|
||||
{{- $keyAdd := 0 -}}
|
||||
|
||||
{{- range $field := .Field.Children -}}
|
||||
{{- form_row $field -}}
|
||||
{{- if and (not $required) (not $isMultiple) -}}
|
||||
{{- $keyAdd = 1 -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if $isExpanded -}}
|
||||
{{- if and (not $required) (not $isMultiple) -}}
|
||||
<input value="" {{ if not $field.Data }}checked{{ end }} name="{{ $field.GetName }}" type="radio" id="{{ $field.GetId }}-0">
|
||||
<label for="{{ $field.GetId }}-0">None</label>
|
||||
{{- end -}}
|
||||
|
||||
{{- range $key, $choice := $choices.GetChoices -}}
|
||||
<input name="{{ $field.GetName }}" type="{{ if $isMultiple }}checkbox{{ else }}radio{{ end }}" value="{{ $choice.Value }}" {{ if $choices.Match $field $choice.Value }}checked{{ end }} id="{{ $field.GetId }}-{{ sum $key $keyAdd }}">
|
||||
<label for="{{ $field.GetId }}-{{ sum $key $keyAdd }}">{{- $choice.Label -}}</label>
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
<select id="{{ .Field.GetId }}" {{ if $required }}required="required"{{ end }} {{ if $isMultiple }}multiple{{ end }} name="{{ .Field.GetName }}" {{ form_widget_attr .Field }}>
|
||||
{{- if not $required -}}
|
||||
<option value="">{{ $emptyChoiceLabel }}</option>
|
||||
{{- end -}}
|
||||
{{- range $choice := $choices.GetChoices -}}
|
||||
<option value="{{ $choice.Value }}" {{ if $choices.Match $field $choice.Value }}selected{{ end }}>{{ $choice.Label }}</option>
|
||||
{{- end -}}
|
||||
</select>
|
||||
{{- end -}}
|
||||
`,
|
||||
"sub_form": `
|
||||
<fieldset id="{{ .Field.GetId }}">
|
||||
{{ if .Field.HasOption "label" }}
|
||||
{{ $label := (.Field.GetOption "label").Value }}
|
||||
|
||||
{{- if ne $label "" -}}
|
||||
<legend {{ form_label_attr .Field }}>{{ $label }}</legend>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{ form_widget_help .Field }}
|
||||
|
||||
{{- range $field := .Field.Children -}}
|
||||
{{- form_row $field -}}
|
||||
{{- end -}}
|
||||
</fieldset>
|
||||
`,
|
||||
"error": `
|
||||
{{- if gt (len .Errors) 0 -}}
|
||||
|
|
@ -49,9 +101,19 @@ var Html5 = map[string]string{
|
|||
{{- end -}}
|
||||
`,
|
||||
"row": `<div class="row">
|
||||
{{- form_label .Field -}}
|
||||
{{ $labelAfterWidget := and (.Field.HasOption "type") (eq (.Field.GetOption "type").Value "checkbox") }}
|
||||
|
||||
{{ if and (eq (len .Field.Children) 0) (not $labelAfterWidget) }}
|
||||
{{- form_label .Field -}}
|
||||
{{ end }}
|
||||
|
||||
{{- form_error nil .Field -}}
|
||||
{{- form_widget .Field -}}
|
||||
|
||||
{{ if and (eq (len .Field.Children) 0) ($labelAfterWidget) }}
|
||||
{{- form_label .Field -}}
|
||||
{{ end }}
|
||||
|
||||
{{- form_widget_help .Field -}}
|
||||
</div>`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"html/template"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"gitnet.fr/deblan/go-form/form"
|
||||
"gitnet.fr/deblan/go-form/validation"
|
||||
)
|
||||
|
|
@ -136,8 +137,8 @@ func (r *Renderer) RenderAttr(name, tpl string, args any) 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{
|
||||
func (r *Renderer) FuncMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"form": r.RenderForm,
|
||||
"form_row": r.RenderRow,
|
||||
"form_label": r.RenderLabel,
|
||||
|
|
@ -148,7 +149,19 @@ func (r *Renderer) Render(name, tpl string, args any) template.HTML {
|
|||
"form_label_attr": r.RenderLabelAttr,
|
||||
"form_help": r.RenderFormHelp,
|
||||
"form_widget_help": r.RenderWidgetHelp,
|
||||
}).Parse(tpl)
|
||||
"sum": func(values ...any) float64 {
|
||||
res := float64(0)
|
||||
for _, value := range values {
|
||||
res += cast.ToFloat64(value)
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) Render(name, tpl string, args any) template.HTML {
|
||||
t, err := template.New(name).Funcs(r.FuncMap()).Parse(tpl)
|
||||
|
||||
if err != nil {
|
||||
return template.HTML(err.Error())
|
||||
|
|
|
|||
25
validation/mail.go
Normal file
25
validation/mail.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package validation
|
||||
|
||||
import "net/mail"
|
||||
|
||||
type Mail struct {
|
||||
}
|
||||
|
||||
func (c Mail) Validate(data any) []Error {
|
||||
errors := []Error{}
|
||||
|
||||
notBlank := NotBlank{}
|
||||
nbErrs := notBlank.Validate(data)
|
||||
|
||||
if len(nbErrs) > 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
_, err := mail.ParseAddress(data.(string))
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, Error("This value is not a valid email address."))
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package validation
|
|||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type NotBlank struct {
|
||||
|
|
@ -9,33 +11,48 @@ type NotBlank struct {
|
|||
|
||||
func (c NotBlank) Validate(data any) []Error {
|
||||
isValid := true
|
||||
label := "This value should not be blank."
|
||||
errors := []Error{}
|
||||
|
||||
if data == nil {
|
||||
errors = append(errors, Error(label))
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
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 {
|
||||
errors = append(errors, Error("This value can not be processed"))
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
isValid = data == false
|
||||
case reflect.Array:
|
||||
case reflect.Slice:
|
||||
isValid = reflect.ValueOf(data).Len() > 0
|
||||
case reflect.String:
|
||||
isValid = len(data.(string)) > 0
|
||||
case reflect.Float32:
|
||||
case reflect.Float64:
|
||||
case reflect.Int:
|
||||
case reflect.Int16:
|
||||
case reflect.Int32:
|
||||
case reflect.Int64:
|
||||
case reflect.Int8:
|
||||
case reflect.Uint:
|
||||
case reflect.Uint16:
|
||||
case reflect.Uint32:
|
||||
case reflect.Uint64:
|
||||
case reflect.Uint8:
|
||||
isValid = cast.ToFloat64(data.(string)) == float64(0)
|
||||
default:
|
||||
errors = append(errors, Error("This value can not be processed."))
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
errors = append(errors, Error("This value should be blank"))
|
||||
errors = append(errors, Error(label))
|
||||
}
|
||||
|
||||
return errors
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue