diff --git a/example/address.go b/example/address.go deleted file mode 100644 index ef58cf6..0000000 --- a/example/address.go +++ /dev/null @@ -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!"), - ) -} diff --git a/example/form.go b/example/form.go new file mode 100644 index 0000000..3767dc3 --- /dev/null +++ b/example/form.go @@ -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!"), + ) +} diff --git a/form/field.go b/form/field.go index b96980e..0e184d3 100644 --- a/form/field.go +++ b/form/field.go @@ -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 { diff --git a/form/field_checkbox.go b/form/field_checkbox.go new file mode 100644 index 0000000..7fc3947 --- /dev/null +++ b/form/field_checkbox.go @@ -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 +} diff --git a/form/field_choice.go b/form/field_choice.go new file mode 100644 index 0000000..3040b8b --- /dev/null +++ b/form/field_choice.go @@ -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 +} diff --git a/form/field_input.go b/form/field_input.go index 2b5ee58..0996d1e 100644 --- a/form/field_input.go +++ b/form/field_input.go @@ -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" diff --git a/form/field_input_date.go b/form/field_input_date.go new file mode 100644 index 0000000..5a4a257 --- /dev/null +++ b/form/field_input_date.go @@ -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 +} diff --git a/form/form.go b/form/form.go index 3c218af..139d5aa 100644 --- a/form/form.go +++ b/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())) + } } } diff --git a/go.mod b/go.mod index 4377854..938ce07 100644 --- a/go.mod +++ b/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 +) diff --git a/go.sum b/go.sum index a02d85d..9bdda7b 100644 --- a/go.sum +++ b/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= diff --git a/main.go b/main.go index 7b70eaa..8bb8c85 100644 --- a/main.go +++ b/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(` + + +
+ +