diff --git a/example/form.go b/example/form.go
index 458f33b..4fd035c 100644
--- a/example/form.go
+++ b/example/form.go
@@ -36,12 +36,18 @@ type ExampleDates struct {
}
type ExampleData struct {
- Bytes []byte
- Text string
- Checkbox bool
- Dates ExampleDates
- Choices ExampleChoices
- Inputs ExampleOtherInputs
+ Collection []CollectionItem
+ Bytes []byte
+ Text string
+ Checkbox bool
+ Dates ExampleDates
+ Choices ExampleChoices
+ Inputs ExampleOtherInputs
+}
+
+type CollectionItem struct {
+ ValueA string
+ ValueB string
}
func CreateDataForm() *form.Form {
@@ -60,6 +66,19 @@ func CreateDataForm() *form.Form {
})
return form.NewForm(
+ form.NewFieldCollection("Collection").
+ WithOptions(
+ form.NewOption("label", "Collection"),
+ form.NewOption("form", form.NewForm(
+ form.NewFieldText("ValueA").
+ WithOptions(form.NewOption("label", "Value A")).
+ WithConstraints(
+ validation.NewNotBlank(),
+ ),
+ form.NewFieldText("ValueB").
+ WithOptions(form.NewOption("label", "Value B")),
+ )),
+ ),
form.NewFieldText("Bytes").
WithOptions(
form.NewOption("label", "Bytes"),
diff --git a/form/field.go b/form/field.go
index 1a3f5e5..dee37bf 100644
--- a/form/field.go
+++ b/form/field.go
@@ -27,6 +27,14 @@ import (
// Generic function for field.Validation
func FieldValidation(f *Field) bool {
+ // if f.IsCollection {
+ // if formOption := f.GetOption("form"); formOption != nil {
+ // if formValue, ok := formOption.Value.(*Form); ok {
+ // godump.Dump(formValue)
+ // }
+ // }
+ // }
+
if len(f.Children) > 0 {
isValid := true
@@ -56,20 +64,22 @@ func FieldValidation(f *Field) bool {
// Field represents a field in a form
type Field struct {
- Name string `json:"name"`
- Widget string `json:"widget"`
- Data any `json:"-"`
- Options []*Option `json:"options"`
- Children []*Field `json:"children"`
- Constraints []validation.Constraint `json:"-"`
- Errors []validation.Error `json:"-"`
- BeforeMount func(data any) (any, error) `json:"-"`
- BeforeBind func(data any) (any, error) `json:"-"`
- Validate func(f *Field) bool `json:"-"`
- IsSlice bool `json:"is_slice"`
- IsFixedName bool `json:"is_fixed_name"`
- Form *Form `json:"-"`
- Parent *Field `json:"-"`
+ Name string `json:"name"`
+ NamePrefix string `json:"name_prefix"`
+ Widget string `json:"widget"`
+ Data any `json:"-"`
+ Options []*Option `json:"options"`
+ Children []*Field `json:"children"`
+ Constraints []validation.Constraint `json:"-"`
+ Errors []validation.Error `json:"-"`
+ BeforeMount func(data any) (any, error) `json:"-"`
+ BeforeBind func(data any) (any, error) `json:"-"`
+ Validate func(f *Field) bool `json:"-"`
+ IsSlice bool `json:"is_slice"`
+ IsCollection bool `json:"is_collection"`
+ IsFixedName bool `json:"is_fixed_name"`
+ Form *Form `json:"-"`
+ Parent *Field `json:"-"`
}
// Generates a new field with default properties
@@ -102,6 +112,20 @@ func NewField(name, widget string) *Field {
return f
}
+func (f *Field) Copy() *Field {
+ return &Field{
+ Name: f.Name,
+ Widget: f.Widget,
+ Options: f.Options,
+ Constraints: f.Constraints,
+ BeforeMount: f.BeforeMount,
+ BeforeBind: f.BeforeBind,
+ Validate: f.Validate,
+ IsSlice: f.IsSlice,
+ IsFixedName: f.IsFixedName,
+ }
+}
+
// Checks if the field contains an option using its name
func (f *Field) HasOption(name string) bool {
for _, option := range f.Options {
@@ -137,6 +161,20 @@ func (f *Field) WithOptions(options ...*Option) *Field {
return f
}
+func (f *Field) RemoveOption(name string) *Field {
+ var options []*Option
+
+ for _, option := range f.Options {
+ if option.Name != name {
+ options = append(options, option)
+ }
+ }
+
+ f.Options = options
+
+ return f
+}
+
// Sets data the field
func (f *Field) WithData(data any) *Field {
f.Data = data
@@ -158,6 +196,13 @@ func (f *Field) WithSlice() *Field {
return f
}
+// Sets that the field represents a collection
+func (f *Field) WithCollection() *Field {
+ f.IsCollection = true
+
+ return f
+}
+
// Sets that the name of the field is not computed
func (f *Field) WithFixedName() *Field {
f.IsFixedName = true
@@ -233,9 +278,9 @@ func (f *Field) GetName() string {
}
if f.Form != nil && f.Form.Name != "" {
- name = fmt.Sprintf("%s[%s]", f.Form.Name, f.Name)
+ name = fmt.Sprintf("%s%s[%s]", f.Form.Name, f.NamePrefix, f.Name)
} else if f.Parent != nil {
- name = fmt.Sprintf("%s[%s]", f.Parent.GetName(), f.Name)
+ name = fmt.Sprintf("%s%s[%s]", f.Parent.GetName(), f.NamePrefix, f.Name)
} else {
name = f.Name
}
diff --git a/form/field_collection.go b/form/field_collection.go
new file mode 100644
index 0000000..436ea27
--- /dev/null
+++ b/form/field_collection.go
@@ -0,0 +1,73 @@
+package form
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// @license GNU AGPL version 3 or any later version
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+// Generates a sub form
+func NewFieldCollection(name string) *Field {
+ f := NewField(name, "collection").
+ WithOptions(
+ NewOption("allow_add", true),
+ NewOption("allow_delete", true),
+ NewOption("form", nil),
+ ).
+ WithCollection()
+
+ f.WithBeforeMount(func(data any) (any, error) {
+ if opt := f.GetOption("form"); opt != nil {
+ if src, ok := opt.Value.(*Form); ok {
+ src.Name = f.GetName()
+ t := reflect.TypeOf(data)
+
+ switch t.Kind() {
+ case reflect.Slice:
+ slice := reflect.ValueOf(data)
+
+ for i := 0; i < slice.Len(); i++ {
+ form := src.Copy()
+ form.Mount(slice.Index(i).Interface())
+
+ field := f.Copy()
+ field.Widget = "sub_form"
+ field.Name = fmt.Sprintf("%d", i)
+ field.Add(form.Fields...)
+ field.
+ RemoveOption("form").
+ RemoveOption("label")
+
+ for _, c := range field.Children {
+ c.NamePrefix = fmt.Sprintf("[%d]", i)
+ }
+
+ f.Add(field)
+ }
+ }
+ }
+ }
+
+ return data, nil
+ })
+
+ return f
+}
+
+func NewCollection(name string) *Field {
+ return NewFieldCollection(name)
+}
diff --git a/form/form.go b/form/form.go
index a2dda2a..b96df95 100644
--- a/form/form.go
+++ b/form/form.go
@@ -22,8 +22,10 @@ import (
"net/http"
"net/url"
"slices"
+ "strings"
"github.com/mitchellh/mapstructure"
+ "github.com/yassinebenaid/godump"
"gitnet.fr/deblan/go-form/util"
"gitnet.fr/deblan/go-form/validation"
)
@@ -108,8 +110,10 @@ func (f *Form) End() *Form {
func (f *Form) AddGlobalField(field *Field) {
f.GlobalFields = append(f.GlobalFields, field)
- for _, c := range field.Children {
- f.AddGlobalField(c)
+ if field.Widget != "collection" {
+ for _, c := range field.Children {
+ f.AddGlobalField(c)
+ }
}
}
@@ -220,6 +224,8 @@ func (f *Form) Bind(data any) error {
field.Bind(toBind, nil)
}
+ godump.Dump(toBind)
+
return mapstructure.Decode(toBind, data)
}
@@ -255,8 +261,23 @@ func (f *Form) HandleRequest(req *http.Request) {
isSubmitted := false
+ type collectionData map[string]any
+
for _, c := range f.GlobalFields {
- if data.Has(c.GetName()) {
+ if c.IsCollection {
+ collection := util.NewCollection()
+
+ for key, _ := range data {
+ if strings.HasPrefix(key, c.GetName()) {
+ root := strings.Replace(key, c.GetName(), "", 1)
+ indexes := util.ExtractDataIndexes(root)
+
+ collection.Add(indexes, data.Get(key))
+ }
+ }
+
+ c.Mount(collection.Slice())
+ } else if data.Has(c.GetName()) {
isSubmitted = true
if c.IsSlice {
@@ -289,3 +310,16 @@ func (f *Form) ErrorsTree() map[string]any {
"children": slices.Collect(maps.Values(errors)),
}
}
+
+func (f *Form) Copy() *Form {
+ var fields []*Field
+
+ for _, i := range f.Fields {
+ f := *i
+ fields = append(fields, &f)
+ }
+
+ return &Form{
+ Fields: fields,
+ }
+}
diff --git a/main.go b/main.go
index 55e9229..da58d53 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "encoding/json"
"html/template"
"log"
"net/http"
@@ -13,6 +14,11 @@ import (
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := example.ExampleData{}
+ data.Collection = []example.CollectionItem{
+ {"Value a 1", "Value b 1"},
+ {"Value a 2", "Value b 2"},
+ {"Value a 3", "Value b 3"},
+ }
f := example.CreateDataForm()
f.Mount(data)
@@ -22,7 +28,7 @@ func main() {
if f.IsSubmitted() && f.IsValid() {
f.Bind(&data)
- godump.Dump(data)
+ // godump.Dump(data)
}
}
@@ -91,10 +97,19 @@ func main() {
Valid
{{ .Form.IsValid }}
-
+
+
+
+ {{ .Dump }}
+ |
+
+
+ JSON
+ {{ .Json }}
+
+ |
+
+
{{ form .Form }}
@@ -105,9 +120,12 @@ func main() {
var dump godump.Dumper
dump.Theme = godump.Theme{}
+ j, _ := json.MarshalIndent(f, " ", " ")
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tpl.Execute(w, map[string]any{
"Form": f,
+ "Json": string(j),
"Dump": template.HTML(dump.Sprint(data)),
})
})
diff --git a/theme/html5.go b/theme/html5.go
index a441e48..a709e4e 100644
--- a/theme/html5.go
+++ b/theme/html5.go
@@ -1,6 +1,7 @@
package theme
import (
+ "bytes"
"fmt"
"github.com/spf13/cast"
@@ -348,6 +349,53 @@ var Html5 = CreateTheme(func() map[string]RenderFunc {
)
}
+ theme["collection"] = func(parent map[string]RenderFunc, args ...any) Node {
+ field := args[0].(*form.Field)
+
+ var prototype string
+
+ if opt := field.GetOption("form"); opt != nil {
+ if val, ok := opt.Value.(*form.Form); ok {
+ var buffer bytes.Buffer
+ dest := form.NewFieldSubForm(field.Name)
+
+ for _, c := range val.Fields {
+ child := c.Copy()
+ child.NamePrefix = "[__name__]"
+ dest.Add(child)
+ }
+
+ fieldPrototype := parent["form_row"](parent, dest)
+ fieldPrototype.Render(&buffer)
+
+ prototype = buffer.String()
+ }
+ }
+
+ field.WithOptions(form.NewOption("prototype", prototype))
+ field.Widget = "collection_build"
+
+ return Div(
+ Attr("data-prototype", prototype),
+ parent["form_widget"](parent, field),
+ )
+ }
+
+ theme["collection_build"] = func(parent map[string]RenderFunc, args ...any) Node {
+ field := args[0].(*form.Field)
+ prototype := field.GetOption("prototype").AsString()
+ var items []Node
+
+ for _, child := range field.Children {
+ items = append(items, parent["form_row"](parent, child))
+ }
+
+ return Div(
+ Attr("data-prototype", prototype),
+ Group(items),
+ )
+ }
+
theme["form_widget"] = func(parent map[string]RenderFunc, args ...any) Node {
field := args[0].(*form.Field)
diff --git a/util/collection.go b/util/collection.go
new file mode 100644
index 0000000..7ee4d66
--- /dev/null
+++ b/util/collection.go
@@ -0,0 +1,96 @@
+package util
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/spf13/cast"
+)
+
+type CollectionValue struct {
+ Name string
+ Value string
+ Children map[string]*CollectionValue
+}
+
+type Collection struct {
+ Children map[int]*CollectionValue
+}
+
+func NewCollection() *Collection {
+ return &Collection{
+ Children: make(map[int]*CollectionValue),
+ }
+}
+
+func NewCollectionValue(name string) *CollectionValue {
+ return &CollectionValue{
+ Name: name,
+ Children: make(map[string]*CollectionValue),
+ }
+}
+
+func (c *Collection) Add(indexes []string, value string) {
+ firstIndex := cast.ToInt(indexes[0])
+ size := len(indexes)
+ child := c.Children[firstIndex]
+
+ if child == nil {
+ child = NewCollectionValue(indexes[0])
+ c.Children[firstIndex] = child
+ }
+
+ child.Add(indexes[1:size], value, nil)
+}
+
+func (c *Collection) Slice() []any {
+ var result []any
+
+ for _, child := range c.Children {
+ result = append(result, child.Map())
+ }
+
+ return result
+}
+
+func (c *CollectionValue) Map() any {
+ if len(c.Children) == 0 {
+ return c.Value
+ }
+
+ results := make(map[string]any)
+
+ for _, child := range c.Children {
+ results[child.Name] = child.Map()
+ }
+
+ return results
+}
+
+func (c *CollectionValue) Add(indexes []string, value string, lastChild *CollectionValue) {
+ size := len(indexes)
+
+ if size > 0 {
+ firstIndex := indexes[0]
+ child := c.Children[firstIndex]
+
+ child = NewCollectionValue(indexes[0])
+ c.Children[firstIndex] = child
+
+ child.Add(indexes[1:size], value, child)
+ } else {
+ lastChild.Value = value
+ }
+}
+
+func ExtractDataIndexes(value string) []string {
+ re := regexp.MustCompile(`\[[^\]]+\]`)
+ items := re.FindAll([]byte(value), -1)
+ var results []string
+
+ for _, i := range items {
+ results = append(results, strings.Trim(string(i), "[]"))
+ }
+
+ return results
+}
diff --git a/util/inspect.go b/util/inspect.go
index 17c0839..22fa4f4 100644
--- a/util/inspect.go
+++ b/util/inspect.go
@@ -29,6 +29,10 @@ func InspectStruct(input interface{}) (map[string]interface{}, error) {
val = val.Elem()
}
+ if val.Kind() == reflect.Map {
+ return input.(map[string]interface{}), nil
+ }
+
if val.Kind() != reflect.Struct {
return nil, errors.New("Invalid type")
}