[WIP] feat: add collection widget
This commit is contained in:
parent
f25f823265
commit
a06afe583d
8 changed files with 367 additions and 30 deletions
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
73
form/field_collection.go
Normal file
73
form/field_collection.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// 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)
|
||||
}
|
||||
40
form/form.go
40
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
main.go
28
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() {
|
|||
<strong>Valid</strong>
|
||||
<span class="debug-value">{{ .Form.IsValid }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Data</strong>
|
||||
<pre class="debug-valid">{{ .Dump }}</pre>
|
||||
</div>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<pre class="debug-valid">{{ .Dump }}</pre>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
<pre class="debug-valid">{{ .Json }}</pre>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{ 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)),
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
96
util/collection.go
Normal file
96
util/collection.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue