diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba56fe5..f207094 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,37 +1,5 @@
## [Unreleased]
-## v1.5.0
-
-### Added
-
-- feat: add collection widget
-- feat: refactoring and improvement of example
-
-## v1.4.0
-
-### Added
-
-- feat: add json configuration
-
-### Fixed
-
-- fix: reset `GlobalFields` in `End()`
-
-## v1.3.0
-
-### Added
-
-- feat: allow to handle request using json body"
-- feat: add `WithJsonRequest` on form
-- feat: add `MapToUrlValues` transformer
-
-## v1.2.0
-
-### Added
-
-- feat: add tag `field` to specify the naming strategy (case)
-- feat: add `ErrorsTree` methods on form and field
-
## v1.1.6
### Fixed
diff --git a/README.md b/README.md
index 3754083..767ec58 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,9 @@
Creating and processing HTML forms is hard and repetitive. You need to deal with rendering HTML form fields, validating submitted data, mapping the form data into objects and a lot more. [`go-form`][go-form] includes a powerful form feature that provides all these features.
-[`go-form`][go-form] is heavily influenced by [Symfony Form](https://symfony.com/doc/current/forms.html). It includes:
+## Introduction
+
+[`go-form`][go-form] is heavily influenced by [Symfony Form](https://symfony.com/doc/current/forms.html). It includes:
* A form builder based on fields declarations and independent of structs
* Validation based on constraints
@@ -10,11 +12,70 @@ Creating and processing HTML forms is hard and repetitive. You need to deal with
* Data binding to populate a struct instance from a submitted form
* Form renderer with customizable themes
-## Documentation
+## Installation
-* [Official documentation][doc]
-* [Demo][demo]
+```shell
+go get gitnet.fr/deblan/go-form
+```
+
+## Quick Start
+
+```go
+package main
+
+import (
+ "html/template"
+ "log"
+ "net/http"
+
+ "gitnet.fr/deblan/go-form/form"
+ "gitnet.fr/deblan/go-form/theme"
+ "gitnet.fr/deblan/go-form/validation"
+)
+
+func main() {
+ type Person struct {
+ Name string
+ }
+
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ data := new(Person)
+
+ f := form.NewForm(
+ form.NewFieldText("Name").
+ WithOptions(
+ form.NewOption("label", "Your name"),
+ ).
+ WithConstraints(
+ validation.NewNotBlank(),
+ ),
+ form.NewSubmit("submit"),
+ ).
+ End().
+ WithMethod(http.MethodPost).
+ WithAction("/")
+
+ f.Mount(data)
+
+ if r.Method == f.Method {
+ f.HandleRequest(r)
+
+ if f.IsSubmitted() && f.IsValid() {
+ f.Bind(data)
+ }
+ }
+
+ render := theme.NewRenderer(theme.Html5)
+ tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`)
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ tpl.Execute(w, map[string]any{
+ "Form": f,
+ })
+ })
+
+ log.Fatal(http.ListenAndServe(":1324", nil))
+}
+```
[go-form]: https://gitnet.fr/deblan/go-form
-[demo]: https://gitnet.fr/deblan/go-form-demo
-[doc]: https://deblan.gitnet.page/go-form/
diff --git a/example.go b/example.go
deleted file mode 100644
index 91918d4..0000000
--- a/example.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package main
-
-import (
- "embed"
- "encoding/json"
- "html/template"
- "log"
- "net/http"
-
- "github.com/yassinebenaid/godump"
- "gitnet.fr/deblan/go-form/example"
- "gitnet.fr/deblan/go-form/theme"
-)
-
-//go:embed example/view/*.html
-var templates embed.FS
-
-func handler(view, action string, formRenderer *theme.Renderer, w http.ResponseWriter, r *http.Request) {
- entity := example.ExampleData{}
- entityForm := example.CreateDataForm(action)
- entityForm.Mount(entity)
-
- style := example.NewTheme(action)
- styleForm := example.CreateThemeSelectorForm()
- styleForm.Mount(style)
-
- if r.Method == entityForm.Method {
- entityForm.HandleRequest(r)
-
- if entityForm.IsSubmitted() && entityForm.IsValid() {
- entityForm.Bind(&entity)
- }
- }
-
- content, _ := templates.ReadFile(view)
-
- formAsJson, _ := json.MarshalIndent(entityForm, " ", " ")
-
- tpl, _ := template.New("page").
- Funcs(formRenderer.FuncMap()).
- Parse(string(content))
-
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
-
- var dump godump.Dumper
- dump.Theme = godump.Theme{}
-
- tpl.Execute(w, map[string]any{
- "isSubmitted": entityForm.IsSubmitted(),
- "isValid": entityForm.IsValid(),
- "form": entityForm,
- "styleForm": styleForm,
- "json": string(formAsJson),
- "dump": template.HTML(dump.Sprint(entity)),
- })
-}
-
-func main() {
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- handler(
- "example/view/html5.html",
- "/",
- theme.NewRenderer(theme.Html5),
- w,
- r,
- )
- })
-
- http.HandleFunc("/bootstrap", func(w http.ResponseWriter, r *http.Request) {
- handler(
- "example/view/bootstrap.html",
- "/bootstrap",
- theme.NewRenderer(theme.Bootstrap5),
- w,
- r,
- )
- })
-
- log.Println("Browse: http://localhost:1122")
- log.Fatal(http.ListenAndServe(":1122", nil))
-}
diff --git a/example/form.go b/example/form.go
index fc6e8bf..458f33b 100644
--- a/example/form.go
+++ b/example/form.go
@@ -36,25 +36,15 @@ type ExampleDates struct {
}
type ExampleData struct {
- Collection []CollectionItem
- Bytes []byte
- Text string
- Checkbox bool
- Dates ExampleDates
- Choices ExampleChoices
- Inputs ExampleOtherInputs
+ Bytes []byte
+ Text string
+ Checkbox bool
+ Dates ExampleDates
+ Choices ExampleChoices
+ Inputs ExampleOtherInputs
}
-type CollectionItem struct {
- ValueA string
- ValueB string
-}
-
-type Theme struct {
- Value string `field:"lowerCamel"`
-}
-
-func CreateDataForm(action string) *form.Form {
+func CreateDataForm() *form.Form {
items := []Item{
Item{Id: 1, Name: "Item 1"},
Item{Id: 2, Name: "Item 2"},
@@ -187,19 +177,6 @@ func CreateDataForm(action string) *form.Form {
form.NewOption("multiple", true),
),
),
- 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.NewFieldCsrf("_csrf_token").WithData("my-token"),
form.NewSubmit("submit").
WithOptions(
@@ -210,42 +187,8 @@ func CreateDataForm(action string) *form.Form {
).
End().
WithOptions(
- form.NewOption("help", "Form global help"),
+ form.NewOption("help", "Form help"),
).
WithMethod(http.MethodPost).
- WithAction(action)
-}
-
-func NewTheme(value string) *Theme {
- return &Theme{Value: value}
-}
-
-func CreateThemeSelectorForm() *form.Form {
- choices := form.NewChoices([]map[string]string{
- map[string]string{"value": "/", "label": "Html5"},
- map[string]string{"value": "/bootstrap", "label": "Bootstrap5"},
- })
-
- choices.LabelBuilder = func(key int, item any) string {
- return item.(map[string]string)["label"]
- }
-
- choices.ValueBuilder = func(key int, item any) string {
- return item.(map[string]string)["value"]
- }
-
- return form.NewForm(
- form.NewFieldChoice("value").
- WithOptions(
- form.NewOption("choices", choices),
- form.NewOption("label", "Select a theme"),
- form.NewOption("required", true),
- form.NewOption("attr", form.Attrs{
- "onchange": "document.location.href = this.value",
- }),
- ),
- ).
- End().
- WithName("").
- WithMethod(http.MethodGet)
+ WithAction("/")
}
diff --git a/example/view/bootstrap.html b/example/view/bootstrap.html
deleted file mode 100644
index 23543fd..0000000
--- a/example/view/bootstrap.html
+++ /dev/null
@@ -1,119 +0,0 @@
-
-
-
-
- Demo of go-form with Bootstrap
-
-
-
-
-
- {{ form_widget (.styleForm.GetField "value") }}
-
-
-
Demo of go-form with Bootstrap
-
-
- Debug view
-
- Submitted:
- {{ .isSubmitted }}
-
-
- Valid:
- {{ .isValid }}
-
-
-
- Dump of data
- {{ .dump }}
-
-
-
- Form as JSON
- {{ .json }}
-
-
-
- {{if .isValid}}
-
The form is valid!
- {{else}}
-
The form is invalid!
- {{end}}
-
- {{ form .form }}
-
-
-
-
-
diff --git a/example/view/html5.html b/example/view/html5.html
deleted file mode 100644
index 88db2d8..0000000
--- a/example/view/html5.html
+++ /dev/null
@@ -1,138 +0,0 @@
-
-
-
-
- Demo of go-form (pure HTML5 and Pico)
-
-
-
-
-
-
-
-
- {{ form_widget (.styleForm.GetField "value") }}
-
-
- Demo of go-form (pure HTML5 and Pico)
-
-
- Debug view
-
-
- Submitted:
- {{ .isSubmitted }}
-
-
- Valid:
- {{ .isValid }}
-
-
-
- Dump of data
- {{ .dump }}
-
-
-
- Form as JSON
- {{ .json }}
-
-
-
-
- {{if .isValid}}
- The form is valid!
- {{else}}
- The form is invalid!
- {{end}}
-
- {{ form .form }}
-
-
-
-
-
diff --git a/form/field.go b/form/field.go
index fe63326..7abae57 100644
--- a/form/field.go
+++ b/form/field.go
@@ -17,8 +17,6 @@ package form
import (
"fmt"
- "maps"
- "slices"
"strings"
"gitnet.fr/deblan/go-form/util"
@@ -26,7 +24,7 @@ import (
)
// Generic function for field.Validation
-func DefaultFieldValidation(f *Field) bool {
+func FieldValidation(f *Field) bool {
if len(f.Children) > 0 {
isValid := true
@@ -38,11 +36,7 @@ func DefaultFieldValidation(f *Field) bool {
c.Errors = errs
}
- isValid = isChildValid && isValid
-
- for _, sc := range c.Children {
- isValid = DefaultFieldValidation(sc) && isValid
- }
+ isValid = isValid && isChildValid
}
return isValid
@@ -60,22 +54,20 @@ func DefaultFieldValidation(f *Field) bool {
// Field represents a field in a form
type Field struct {
- 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:"-"`
+ Name string
+ Widget string
+ Data any
+ Options []*Option
+ Children []*Field
+ Constraints []validation.Constraint
+ Errors []validation.Error
+ BeforeMount func(data any) (any, error)
+ BeforeBind func(data any) (any, error)
+ Validate func(f *Field) bool
+ IsSlice bool
+ IsFixedName bool
+ Form *Form
+ Parent *Field
}
// Generates a new field with default properties
@@ -103,26 +95,11 @@ func NewField(name, widget string) *Field {
NewOption("help_attr", Attrs{}),
)
- f.Validate = DefaultFieldValidation
+ f.Validate = FieldValidation
return f
}
-func (f *Field) Copy() *Field {
- return &Field{
- Name: f.Name,
- Form: f.Form,
- 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 {
@@ -158,21 +135,6 @@ func (f *Field) WithOptions(options ...*Option) *Field {
return f
}
-// Remove an option if exists
-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
@@ -194,13 +156,6 @@ 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
@@ -276,9 +231,9 @@ func (f *Field) GetName() string {
}
if f.Form != nil && f.Form.Name != "" {
- name = fmt.Sprintf("%s%s[%s]", f.Form.Name, f.NamePrefix, f.Name)
+ name = fmt.Sprintf("%s[%s]", f.Form.Name, f.Name)
} else if f.Parent != nil {
- name = fmt.Sprintf("%s%s[%s]", f.Parent.GetName(), f.NamePrefix, f.Name)
+ name = fmt.Sprintf("%s[%s]", f.Parent.GetName(), f.Name)
} else {
name = f.Name
}
@@ -330,7 +285,7 @@ func (f *Field) Mount(data any) error {
}
// Bind the data into the given map
-func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error {
+func (f *Field) Bind(data map[string]any, key *string) error {
if len(f.Children) == 0 {
v, err := f.BeforeBind(f.Data)
@@ -350,74 +305,8 @@ func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error
data[f.Name] = make(map[string]any)
for _, child := range f.Children {
- child.Bind(data[f.Name].(map[string]any), key, f.IsCollection)
- }
-
- if f.IsCollection {
- var nextData []any
- values := data[f.Name].(map[string]any)
- var keys []string
-
- for key, _ := range values {
- keys = append(keys, key)
- }
-
- slices.Sort(keys)
-
- for _, key := range keys {
- for valueKey, value := range values {
- if valueKey == key {
- nextData = append(nextData, value)
- }
- }
- }
-
- data[f.Name] = nextData
+ child.Bind(data[f.Name].(map[string]any), key)
}
return nil
}
-
-// Generates a tree of errors
-func (f *Field) ErrorsTree(tree map[string]any, key *string) {
- var index string
-
- if key != nil {
- index = *key
- } else {
- index = f.Name
- }
-
- if len(f.Children) == 0 {
- if len(f.Errors) > 0 {
- tree[index] = map[string]any{
- "meta": map[string]any{
- "id": f.GetId(),
- "name": f.Name,
- "formName": f.GetName(),
- },
- "errors": f.Errors,
- }
- }
- } else {
- errors := make(map[string]any)
-
- for _, child := range f.Children {
- if len(child.Errors) > 0 {
- child.ErrorsTree(errors, &child.Name)
- }
- }
-
- if len(errors) > 0 {
- tree[index] = map[string]any{
- "meta": map[string]any{
- "id": f.GetId(),
- "name": f.Name,
- "formName": f.GetName(),
- },
- "errors": []validation.Error{},
- "children": slices.Collect(maps.Values(errors)),
- }
- }
- }
-}
diff --git a/form/field_choice.go b/form/field_choice.go
index 16c06fc..5572344 100644
--- a/form/field_choice.go
+++ b/form/field_choice.go
@@ -16,7 +16,6 @@ package form
// along with this program. If not, see .
import (
- "encoding/json"
"reflect"
"github.com/spf13/cast"
@@ -34,9 +33,9 @@ func (c Choice) Match(value string) bool {
}
type Choices struct {
- Data any `json:"data"`
- ValueBuilder func(key int, item any) string `json:"-"`
- LabelBuilder func(key int, item any) string `json:"-"`
+ 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 {
@@ -101,24 +100,6 @@ func (c *Choices) GetChoices() []Choice {
return choices
}
-func (c Choices) MarshalJSON() ([]byte, error) {
- var choices []map[string]string
-
- 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, map[string]string{
- "value": c.ValueBuilder(i, v.Index(i).Interface()),
- "label": c.LabelBuilder(i, v.Index(i).Interface()),
- })
- }
- }
-
- return json.Marshal(choices)
-}
-
// Generates an instance of Choices
func NewChoices(items any) *Choices {
builder := func(key int, item any) string {
@@ -145,7 +126,7 @@ func NewFieldChoice(name string) *Field {
)
f.Validate = func(field *Field) bool {
- isValid := field.Validate(field)
+ isValid := FieldValidation(field)
if len(validation.NewNotBlank().Validate(field.Data)) == 0 {
choices := field.GetOption("choices").Value.(*Choices)
diff --git a/form/field_collection.go b/form/field_collection.go
deleted file mode 100644
index 3f3ece1..0000000
--- a/form/field_collection.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package form
-
-// @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 .
-
-import (
- "fmt"
- "reflect"
-)
-
-// 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++ {
- name := fmt.Sprintf("%d", i)
- value := slice.Index(i).Interface()
-
- if f.HasChild(name) {
- f.GetChild(name).Mount(value)
- } else {
- form := src.Copy()
- form.Mount(value)
-
- field := f.Copy()
- field.Widget = "sub_form"
- field.Name = name
- 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 04016d3..742d192 100644
--- a/form/form.go
+++ b/form/form.go
@@ -16,13 +16,8 @@ package form
// along with this program. If not, see .
import (
- "encoding/json"
- "io/ioutil"
- "maps"
"net/http"
"net/url"
- "slices"
- "strings"
"github.com/mitchellh/mapstructure"
"gitnet.fr/deblan/go-form/util"
@@ -31,15 +26,14 @@ import (
// Field represents a form
type Form struct {
- Fields []*Field `json:"children"`
- GlobalFields []*Field `json:"-"`
- Errors []validation.Error `json:"-"`
- Method string `json:"method"`
- JsonRequest bool `json:"json_request"`
- Action string `json:"action"`
- Name string `json:"name"`
- Options []*Option `json:"options"`
- RequestData *url.Values `json:"-"`
+ Fields []*Field
+ GlobalFields []*Field
+ Errors []validation.Error
+ Method string
+ Action string
+ Name string
+ Options []*Option
+ RequestData *url.Values
}
// Generates a new form with default properties
@@ -96,8 +90,6 @@ func (f *Form) Add(fields ...*Field) {
// Configures its children deeply
// This function must be called after adding all fields
func (f *Form) End() *Form {
- f.GlobalFields = []*Field{}
-
for _, c := range f.Fields {
f.AddGlobalField(c)
}
@@ -207,18 +199,12 @@ func (f *Form) Mount(data any) error {
return nil
}
-func (f *Form) WithJsonRequest() *Form {
- f.JsonRequest = true
-
- return f
-}
-
// Copies datas from the form to a struct
func (f *Form) Bind(data any) error {
toBind := make(map[string]any)
for _, field := range f.Fields {
- field.Bind(toBind, nil, false)
+ field.Bind(toBind, nil)
}
return mapstructure.Decode(toBind, data)
@@ -228,51 +214,17 @@ func (f *Form) Bind(data any) error {
func (f *Form) HandleRequest(req *http.Request) {
var data url.Values
- if f.JsonRequest {
- body, err := ioutil.ReadAll(req.Body)
-
- if err != nil {
- return
- }
-
- mapping := make(map[string]any)
- err = json.Unmarshal(body, &mapping)
-
- if err != nil {
- return
- }
-
- data = url.Values{}
- util.MapToUrlValues(&data, f.Name, mapping)
+ if f.Method != "GET" {
+ req.ParseForm()
+ data = req.Form
} else {
- switch f.Method {
- case "GET":
- data = req.URL.Query()
- default:
- req.ParseForm()
- data = req.Form
- }
+ data = req.URL.Query()
}
isSubmitted := false
- type collectionData map[string]any
-
for _, c := range f.GlobalFields {
- 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()) {
+ if data.Has(c.GetName()) {
isSubmitted = true
if c.IsSlice {
@@ -292,30 +244,3 @@ func (f *Form) HandleRequest(req *http.Request) {
func (f *Form) IsSubmitted() bool {
return f.RequestData != nil
}
-
-// Generates a tree of errors
-func (f *Form) ErrorsTree() map[string]any {
- errors := make(map[string]any)
-
- for _, field := range f.Fields {
- field.ErrorsTree(errors, nil)
- }
-
- return map[string]any{
- "errors": f.Errors,
- "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/form/option.go b/form/option.go
index 95eee67..9fccd27 100644
--- a/form/option.go
+++ b/form/option.go
@@ -18,8 +18,8 @@ import "strings"
// along with this program. If not, see .
type Option struct {
- Name string `json:"name"`
- Value any `json:"value"`
+ Name string
+ Value any
}
func NewOption(name string, value any) *Option {
diff --git a/go.mod b/go.mod
index 20dda1e..0b43ae4 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,13 @@ module gitnet.fr/deblan/go-form
go 1.23.0
require (
- github.com/iancoleman/strcase v0.3.0
github.com/mitchellh/mapstructure v1.5.0
github.com/spf13/cast v1.9.2
github.com/yassinebenaid/godump v0.11.1
- maragu.dev/gomponents v1.1.0
+)
+
+require (
+ github.com/samber/lo v1.51.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+ maragu.dev/gomponents v1.1.0 // indirect
)
diff --git a/go.sum b/go.sum
index c6c8a71..fd3f0f9 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
-github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -12,9 +10,13 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
+github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI=
github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
maragu.dev/gomponents v1.1.0 h1:iCybZZChHr1eSlvkWp/JP3CrZGzctLudQ/JI3sBcO4U=
maragu.dev/gomponents v1.1.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..55e9229
--- /dev/null
+++ b/main.go
@@ -0,0 +1,175 @@
+package main
+
+import (
+ "html/template"
+ "log"
+ "net/http"
+
+ "github.com/yassinebenaid/godump"
+ "gitnet.fr/deblan/go-form/example"
+ "gitnet.fr/deblan/go-form/theme"
+)
+
+func main() {
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ data := example.ExampleData{}
+
+ f := example.CreateDataForm()
+ f.Mount(data)
+
+ if r.Method == f.Method {
+ f.HandleRequest(r)
+
+ if f.IsSubmitted() && f.IsValid() {
+ f.Bind(&data)
+ godump.Dump(data)
+ }
+ }
+
+ render := theme.NewRenderer(theme.Html5)
+
+ tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`
+
+
+
+
+ Form
+
+
+
+
+
+ Submitted
+ {{ .Form.IsSubmitted }}
+
+
+ Valid
+ {{ .Form.IsValid }}
+
+
+
+
+ {{ form .Form }}
+
+
+ `)
+
+ var dump godump.Dumper
+ dump.Theme = godump.Theme{}
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ tpl.Execute(w, map[string]any{
+ "Form": f,
+ "Dump": template.HTML(dump.Sprint(data)),
+ })
+ })
+
+ http.HandleFunc("/bootstrap", func(w http.ResponseWriter, r *http.Request) {
+ data := example.ExampleData{}
+
+ f := example.CreateDataForm()
+ f.WithAction("/bootstrap")
+ f.Mount(data)
+
+ if r.Method == f.Method {
+ f.HandleRequest(r)
+
+ if f.IsSubmitted() && f.IsValid() {
+ f.Bind(&data)
+ godump.Dump(data)
+ }
+ }
+
+ render := theme.NewRenderer(theme.Bootstrap5)
+
+ tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`
+
+
+
+
+ Form
+
+
+
+
+
+
+ Submitted
+ {{ .Form.IsSubmitted }}
+
+
+ Valid
+ {{ .Form.IsValid }}
+
+
+
+
+ {{ form .Form }}
+
+
+
+ `)
+
+ var dump godump.Dumper
+ dump.Theme = godump.Theme{}
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ tpl.Execute(w, map[string]any{
+ "Form": f,
+ "Dump": template.HTML(dump.Sprint(data)),
+ })
+ })
+
+ log.Fatal(http.ListenAndServe(":1122", nil))
+}
diff --git a/theme/html5.go b/theme/html5.go
index 2d7889e..a441e48 100644
--- a/theme/html5.go
+++ b/theme/html5.go
@@ -1,7 +1,6 @@
package theme
import (
- "bytes"
"fmt"
"github.com/spf13/cast"
@@ -59,7 +58,6 @@ var Html5 = CreateTheme(func() map[string]RenderFunc {
}
return Ul(
- Class("gf-errors"),
Group(result),
)
}
@@ -95,7 +93,6 @@ var Html5 = CreateTheme(func() map[string]RenderFunc {
}
return Div(
- Class("gf-help"),
Text(help),
extra,
)
@@ -351,50 +348,6 @@ 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 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)
@@ -454,7 +407,6 @@ var Html5 = CreateTheme(func() map[string]RenderFunc {
form := args[0].(*form.Form)
return Form(
- Class("gf-form"),
Action(form.Action),
Method(form.Method),
parent["form_attributes"](parent, form),
diff --git a/util/collection.go b/util/collection.go
deleted file mode 100644
index f0aa649..0000000
--- a/util/collection.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package util
-
-// @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 .
-
-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 22fa4f4..76c8e9f 100644
--- a/util/inspect.go
+++ b/util/inspect.go
@@ -18,8 +18,6 @@ package util
import (
"errors"
"reflect"
-
- "github.com/iancoleman/strcase"
)
func InspectStruct(input interface{}) (map[string]interface{}, error) {
@@ -29,10 +27,6 @@ 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")
}
@@ -43,16 +37,8 @@ func InspectStruct(input interface{}) (map[string]interface{}, error) {
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
value := val.Field(i)
- tags := typ.Field(i).Tag
- name := field.Name
- fieldTag := tags.Get("field")
-
- if fieldTag == "lowerCamel" {
- name = strcase.ToLowerCamel(name)
- }
-
- result[name] = value.Interface()
+ result[field.Name] = value.Interface()
}
return result, nil
diff --git a/util/transformer.go b/util/transformer.go
deleted file mode 100644
index a942fa1..0000000
--- a/util/transformer.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package util
-
-// @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 .
-
-import (
- "fmt"
- "net/url"
-)
-
-func MapToUrlValues(values *url.Values, prefix string, data map[string]any) {
- keyFormater := "%s"
-
- if prefix != "" {
- keyFormater = prefix + "[%s]"
- }
-
- for key, value := range data {
- keyValue := fmt.Sprintf(keyFormater, key)
-
- switch v := value.(type) {
- case string:
- values.Add(keyValue, v)
- case []string:
- case []int:
- case []int32:
- case []int64:
- case []any:
- for _, s := range v {
- values.Add(keyValue, fmt.Sprintf("%v", s))
- }
- case bool:
- if v {
- values.Add(keyValue, "1")
- } else {
- values.Add(keyValue, "0")
- }
- case int, int64, float64:
- values.Add(keyValue, fmt.Sprintf("%v", v))
- case map[string]any:
- MapToUrlValues(values, keyValue, v)
- default:
- }
- }
-}
diff --git a/validation/notblank.go b/validation/notblank.go
index 1962cb2..a417e22 100644
--- a/validation/notblank.go
+++ b/validation/notblank.go
@@ -39,7 +39,7 @@ func (c NotBlank) Validate(data any) []Error {
v := reflect.ValueOf(data)
- if data == nil || v.IsZero() {
+ if v.IsZero() {
errors = append(errors, Error(c.Message))
return errors